ransack_search_element 0.0.2.pre.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a6231963831824c4d45beb2710387ff159f2d8ceff31dfe15905678e95c8a62e
4
+ data.tar.gz: 158c5cb3a5c577d2969030635ad93ff61c25d3820ba76629a147eaa9ad233f9d
5
+ SHA512:
6
+ metadata.gz: e8e734658728bfc5cacafec2e3483191f761d1429f68ba2c44b7377b72b7817a1df39ba47fa1e70d9f778903589e32a9daabc73b490fae96c7369425bdbec11b
7
+ data.tar.gz: 8ccae8b4e6d1c4b164d73f9ed88882bdcfc1c9ebaf95ab940f5bdb4a6dd5fabd8273babcaf71911dc6dcaf3c9ffe072b1af5cfc0d3b0e13334035f6f472b6aef
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Arthur Dzieniszewski
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # RansackSearchElement
2
+
3
+ Generic [Ransack](https://github.com/activerecord-hackery/ransack) backed element
4
+ to give a dynamic frontend for advanced searching and filtering, such as for
5
+ exploratory joins. Still early in development (refactoring?), so contributions
6
+ welcome. This is originally based on a set of React and Bootstrap elements.
7
+
8
+ ## Todo
9
+
10
+ - [x] Write wrapper Rails tags
11
+ - [ ] Rip out React and replace with Web Components
12
+ - [ ] Rip out Bootstrap and replace with leaner default styles (possibly
13
+ customizable with sass?)
14
+ - [ ] Decouple javascript further
15
+ - [ ] Write adapters to interface with other search backends? (big maybe)
16
+
17
+ ## Usage
18
+ Use `ransack_search_tag` in your views, and supply it with more options
19
+ as of yet to be defined.
20
+
21
+ ## Installation
22
+
23
+ ```ruby
24
+ gem 'ransack_search_element'
25
+ ```
26
+
27
+ And then execute:
28
+ ```bash
29
+ $ bundle
30
+ ```
31
+
32
+ Or install it yourself as:
33
+ ```bash
34
+ $ gem install ransack_search_element
35
+ ```
36
+
37
+ You will also need to install the accompanying npm package,
38
+ `ransack-search-element`:
39
+
40
+ ```
41
+ yarn add ransack-search-element
42
+ ```
43
+
44
+ ## Contributing
45
+ Yeah just send it.
46
+
47
+ ![Alt](https://i.imgur.com/JNGUWm2.gif?noredirect)
48
+
49
+ ## License
50
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
9
+
10
+ require "rake/testtask"
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = false
16
+ end
17
+
18
+ task default: :test
@@ -0,0 +1,3 @@
1
+ //= link_directory ../stylesheets/ransack_search_element .css
2
+
3
+ console.log('engine here')
@@ -0,0 +1,19 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
16
+
17
+ html {
18
+ background-color: red;
19
+ }
@@ -0,0 +1,4 @@
1
+ module RansackSearchElement
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module RansackSearchElement
2
+ module ApplicationHelper
3
+ def ransack_search_tag(columns, queries)
4
+ tag.div is: "ransack-search", data: {
5
+ columns: columns,
6
+ queries: queries,
7
+ }.to_json
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,226 @@
1
+ import React, { useMemo, useState } from "react";
2
+ import QueryBuilder, {
3
+ AttributeInfo,
4
+ Filter,
5
+ options,
6
+ } from "../utilities/QueryBuilder";
7
+
8
+ import FilterRow from "./FilterRow";
9
+
10
+ export type FilterDropdownProps = {
11
+ columns: {
12
+ root: {
13
+ [key: string]: AttributeInfo;
14
+ };
15
+ associations: {
16
+ [key: string]: {
17
+ [key: string]: AttributeInfo;
18
+ }[];
19
+ }[];
20
+ };
21
+ queries: {
22
+ [key: string]: string;
23
+ };
24
+ base: string;
25
+ };
26
+
27
+ /**
28
+ * This component handles the generation of filters for searching a model.
29
+ *
30
+ * @param props
31
+ * @returns {JSX.Element}
32
+ */
33
+ export default (props: FilterDropdownProps): JSX.Element => {
34
+ const { columns, queries } = props;
35
+
36
+ const { root: colNames, associations } = columns;
37
+
38
+ const possible_values = Object.keys(colNames).concat(
39
+ associations.map((v) => Object.keys(v)[0]).flat()
40
+ );
41
+
42
+ const queryBuilder = useMemo(() => {
43
+ return new QueryBuilder(colNames, associations);
44
+ }, [columns, queries]);
45
+
46
+ const [filters, setFilters] = useState<{
47
+ [key: string]: Filter[];
48
+ }>(queryBuilder.parseQueries(queries));
49
+
50
+ /**
51
+ * The constructed query string to replace the current location when
52
+ * the user "filters". Consists of compounding attribute value with its
53
+ * comparison in the case of a real attribute for the model, or by compounding
54
+ * the association entity's name, with the attribute, and its comparison in
55
+ * the case of an association.
56
+ */
57
+ const searchValue =
58
+ window.location.pathname +
59
+ "?" +
60
+ Object.entries(filters)
61
+ .map(([k, v]) => {
62
+ return v
63
+ .map((x) => {
64
+ if (x["ass"] && k.indexOf(x["ass"]) !== 0) {
65
+ return (
66
+ `q[${x["ass"] + "_" + k + "_" + x["query"]}]` +
67
+ "=" +
68
+ (x["value"] || "")
69
+ );
70
+ } else {
71
+ return `q[${k + "_" + x["query"]}]` + "=" + (x["value"] || "");
72
+ }
73
+ })
74
+ .join("&");
75
+ })
76
+ .join("&");
77
+
78
+ return (
79
+ <div className="card shadow-sm mb-2" style={{ minHeight: "150px" }}>
80
+ <div className="card-body">
81
+ <div className="card-title">
82
+ <h4>Filters</h4>
83
+ </div>
84
+ {Object.keys(filters).map((v) => {
85
+ return filters[v].map((x, i) => (
86
+ <FilterRow
87
+ key={x}
88
+ idx={i}
89
+ attribute={v}
90
+ filters={filters}
91
+ setFilters={setFilters}
92
+ options={options}
93
+ />
94
+ ));
95
+ })}
96
+ </div>
97
+ <div className="card-footer d-flex align-items-center">
98
+ <div className="d-flex">
99
+ <div className="me-1">
100
+ <a role="button" className="btn btn-primary" href={searchValue}>
101
+ Filter
102
+ </a>
103
+ </div>
104
+ <div className="dropdown me-1">
105
+ <button
106
+ className="btn btn-secondary dropdown-toggle"
107
+ type="button"
108
+ id="dropdownMenuButton"
109
+ data-bs-toggle="dropdown"
110
+ aria-haspopup="true"
111
+ aria-expanded="false"
112
+ >
113
+ Add Filter
114
+ </button>
115
+ <ul className="dropdown-menu" aria-labelledby="dropdownMenuButton">
116
+ {Object.keys(colNames).map((v) => (
117
+ <li key={v}>
118
+ <a
119
+ href=""
120
+ className="dropdown-item"
121
+ onClick={(e) => {
122
+ e.preventDefault();
123
+ let copy = { ...filters };
124
+ const t = {};
125
+ t["query"] = "eq";
126
+ t["type"] = colNames[v]?.type || associations[v]?.type;
127
+ t["label"] = colNames[v]?.label || associations[v]?.label;
128
+
129
+ if (copy[v] instanceof Array) {
130
+ copy[v].push(t);
131
+ } else {
132
+ copy[v] = [];
133
+ copy[v].push(t);
134
+ }
135
+
136
+ setFilters(copy);
137
+ }}
138
+ >
139
+ {colNames[v]?.label || associations[v]?.label}
140
+ </a>
141
+ </li>
142
+ ))}
143
+ {associations?.length > 0 ? (
144
+ <li>
145
+ <hr className="dropdown-divider" />
146
+ </li>
147
+ ) : null}
148
+ {associations?.map((x, i) => {
149
+ const association = Object.keys(associations[i])[0];
150
+ const attributes = associations[i][association];
151
+
152
+ return (
153
+ <div className="dropdown dropend">
154
+ <a
155
+ className="dropdown-item dropdown-toggle"
156
+ href="#"
157
+ id="dropdown-layouts"
158
+ data-bs-toggle="dropdown"
159
+ aria-haspopup="true"
160
+ aria-expanded="false"
161
+ >
162
+ {association}
163
+ </a>
164
+ <div
165
+ className="dropdown-menu"
166
+ aria-labelledby="dropdown-layouts"
167
+ >
168
+ {attributes.map((v) => {
169
+ const attr = Object.keys(v)[0];
170
+ const values = Object.values(v)[0];
171
+
172
+ return (
173
+ <li key={attr}>
174
+ <a
175
+ href=""
176
+ className="dropdown-item"
177
+ onClick={(e) => {
178
+ e.preventDefault();
179
+ let copy = { ...filters };
180
+
181
+ const t = {};
182
+ t["query"] = "eq";
183
+ t["type"] = values.type;
184
+ t["label"] = values.label;
185
+ t["ass"] = values.ass;
186
+
187
+ if (copy[attr] instanceof Array) {
188
+ copy[attr].push(t);
189
+ } else {
190
+ copy[attr] = [];
191
+ copy[attr].push(t);
192
+ }
193
+
194
+ setFilters(copy);
195
+ }}
196
+ >
197
+ {values.label}
198
+ </a>
199
+ </li>
200
+ );
201
+ })}
202
+ </div>
203
+ </div>
204
+ );
205
+ })}
206
+ </ul>
207
+ </div>
208
+ </div>
209
+ <div>
210
+ <a href={window.location.pathname}>
211
+ <button className="btn btn-danger">Reset</button>
212
+ </a>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ );
217
+ };
218
+
219
+ /*
220
+
221
+
222
+ */
223
+
224
+ /*
225
+
226
+ */
@@ -0,0 +1,115 @@
1
+ import React from "react";
2
+ import FormInput from "./FormInput";
3
+
4
+ function getOptions(type) {
5
+ switch (type) {
6
+ case "datetime":
7
+ return [
8
+ ["Equals", "eq"],
9
+ ["Before", "lt"],
10
+ ["After", "gt"],
11
+ ["Before/on", "lteq"],
12
+ ["After/on", "gteq"],
13
+ ];
14
+ case "text":
15
+ return [
16
+ ["Contains", "cont"],
17
+ ["Equals", "eq"],
18
+ ];
19
+ default:
20
+ return [
21
+ ["Contains", "cont"],
22
+ ["Equals", "eq"],
23
+ ["Less than", "lt"],
24
+ ["Greater than", "gt"],
25
+ ["Less than/equal", "lteq"],
26
+ ["Greater than/equal", "gteq"],
27
+ ];
28
+ }
29
+ }
30
+
31
+ export default (props) => {
32
+ const { filters, setFilters, attribute, idx } = props;
33
+
34
+ const { query, value, type, label } = filters[attribute][idx];
35
+
36
+ const options = getOptions(type);
37
+
38
+ return (
39
+ <div
40
+ className="d-flex align-items-center filter-row w-100 mb-3"
41
+ style={{ maxWidth: "650px" }}
42
+ key={attribute}
43
+ >
44
+ <a
45
+ className="border rounded-circle me-4"
46
+ style={{
47
+ height: "35px",
48
+ width: "35px",
49
+ display: "flex",
50
+ justifyContent: "center",
51
+ alignItems: "center",
52
+ }}
53
+ href=""
54
+ onClick={(e) => {
55
+ e.preventDefault();
56
+ let copy = { ...filters };
57
+ delete copy[attribute][idx];
58
+
59
+ setFilters(copy);
60
+ }}
61
+ >
62
+ <i className="fas fa-times" />
63
+ </a>
64
+ <div
65
+ className="d-flex justify-content-start align-items-center me-3 mb-0"
66
+ style={{ minWidth: "100px" }}
67
+ >
68
+ <b>{label}</b>
69
+ </div>
70
+ <div className="w-auto me-3 mb-0">
71
+ <select
72
+ defaultValue={filters[attribute][idx]["query"]}
73
+ className="form-select rounded-pill"
74
+ style={{ minWidth: "100px" }}
75
+ onInput={(e) => {
76
+ const copy = { ...filters };
77
+ copy[attribute][idx] = copy[attribute][idx]
78
+ ? copy[attribute][idx]
79
+ : {};
80
+ copy[attribute][idx]["query"] = e.target.value;
81
+ setFilters(copy);
82
+ }}
83
+ >
84
+ {options.map(([k, val]) => (
85
+ <option
86
+ key={`${k}${val}`}
87
+ defaultValue={`${filters[attribute][idx]["query"] === val}`}
88
+ value={val}
89
+ >
90
+ {k}
91
+ </option>
92
+ ))}
93
+ </select>
94
+ </div>
95
+
96
+ <div className="d-flex mb-0" style={{ minWidth: "100px" }}>
97
+ <FormInput
98
+ idx={idx}
99
+ name={attribute}
100
+ query={filters[attribute][idx]["query"]}
101
+ colType={filters[attribute][idx]["type"]}
102
+ className="form-control me-2"
103
+ filters={filters}
104
+ setFilters={setFilters}
105
+ onInput={(e) => {
106
+ let copy = { ...filters };
107
+ copy[attribute][idx]["value"] = e.target.value;
108
+ setFilters(copy);
109
+ }}
110
+ value={filters[attribute][idx]["value"]}
111
+ />
112
+ </div>
113
+ </div>
114
+ );
115
+ };
@@ -0,0 +1,98 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from "react";
2
+
3
+ import DatePicker from "react-datepicker";
4
+ import { convertToDate } from "../utilities/dateConverter";
5
+
6
+ /**
7
+ * Tries to handle the inputs generically.
8
+ * Unfortunately, lot of timezone stuff because we need to shift to and from the local time zone.
9
+ * @param props
10
+ * @returns {JSX.Element}
11
+ */
12
+ export default (props) => {
13
+ const {
14
+ className,
15
+ style,
16
+ colType,
17
+ value,
18
+ onInput,
19
+ filters,
20
+ setFilters,
21
+ name,
22
+ query,
23
+ idx,
24
+ } = props;
25
+
26
+ const type = useMemo(() => {
27
+ if (colType === "datetime") return "datetime-local";
28
+ if (colType === "integer") return "number";
29
+ if (colType === "text") return "text";
30
+ }, []);
31
+
32
+ const ref = useRef();
33
+
34
+ const [date, setDate] = useState(
35
+ (() => {
36
+ const d = new Date();
37
+ d.setHours(d.getHours() - d.getTimezoneOffset() / 60);
38
+ return d;
39
+ })()
40
+ );
41
+
42
+ useEffect(() => {
43
+ if (colType === "datetime" && value) {
44
+ let copy = { ...filters };
45
+ const d = new Date(value);
46
+ d.setHours(d.getHours() + d.getTimezoneOffset() / 60);
47
+ copy[name][idx]["value"] = convertToDate(d);
48
+ setFilters(copy);
49
+ const adjusted_time = new Date(value);
50
+ adjusted_time.setHours(
51
+ adjusted_time.getHours() + adjusted_time.getTimezoneOffset() / 60
52
+ );
53
+ setDate(adjusted_time);
54
+ } else if (colType === "datetime") {
55
+ let copy = { ...filters };
56
+ copy[name][idx]["value"] = convertToDate(date);
57
+ setFilters(copy);
58
+ const adjusted_time = new Date(date);
59
+ adjusted_time.setHours(
60
+ adjusted_time.getHours() + adjusted_time.getTimezoneOffset() / 60
61
+ );
62
+ setDate(adjusted_time);
63
+ }
64
+ }, []);
65
+
66
+ switch (type) {
67
+ case "datetime-local":
68
+ return (
69
+ <DatePicker
70
+ ref={ref}
71
+ className={`${className} filter-input`}
72
+ selected={date}
73
+ onChange={(e: Date) => {
74
+ let copy = { ...filters };
75
+ copy[name][idx]["value"] = convertToDate(e);
76
+ setFilters(copy);
77
+ setDate(e);
78
+ }}
79
+ />
80
+ );
81
+ break;
82
+
83
+ default:
84
+ return (
85
+ <input
86
+ value={value}
87
+ onInput={(e) => {
88
+ e.preventDefault();
89
+ onInput(e);
90
+ }}
91
+ style={style}
92
+ className={className}
93
+ type={type}
94
+ />
95
+ );
96
+ break;
97
+ }
98
+ };
@@ -0,0 +1,44 @@
1
+ import React from "react";
2
+ import FilterDropdown, {
3
+ FilterDropdownProps,
4
+ } from "./components/FilterDropdown";
5
+ import ReactDOM from "react-dom";
6
+ import registerNestedDropdown from "./utilities/registerNestedDropdown";
7
+
8
+ /**
9
+ * Class to handle wrapping of the
10
+ *
11
+ * TODO: remove dependency on React and Bootstrap
12
+ * NOTE: possibly remove dependency on React by just wrapping around a form
13
+ * with a Stimulus controller. Refactor to expose ERB partials.
14
+ */
15
+ class RansackSearch extends HTMLDivElement {
16
+ constructor() {
17
+ super();
18
+ }
19
+
20
+ connectedCallback() {
21
+ const data: FilterDropdownProps = {
22
+ columns: {
23
+ root: {},
24
+ associations: [],
25
+ },
26
+ queries: [],
27
+ ...(JSON.parse(this.getAttribute("data")) || {}),
28
+ };
29
+
30
+ ReactDOM.render(
31
+ [
32
+ React.createElement(FilterDropdown, {
33
+ ...data,
34
+ }),
35
+ ],
36
+ this,
37
+ () => {
38
+ registerNestedDropdown(this);
39
+ }
40
+ );
41
+ }
42
+ }
43
+
44
+ customElements.define("ransack-search", RansackSearch, { extends: "div" });
@@ -0,0 +1,134 @@
1
+ export type Filter = {
2
+ [key: string]: {
3
+ label: string;
4
+ query: string;
5
+ type: string;
6
+ value: string | number;
7
+ ass?: string;
8
+ };
9
+ };
10
+
11
+ export type AttributeInfo = {
12
+ type: string;
13
+ label: string;
14
+ ass?: string;
15
+ };
16
+
17
+ export const options = [
18
+ ["Contains", "cont"],
19
+ ["Equals", "eq"],
20
+ ["Less than", "lt"],
21
+ ["Greater than", "gt"],
22
+ ["Less than/equal", "lteq"],
23
+ ["Greater than/equal", "gteq"],
24
+ ];
25
+
26
+ const options_values = options.map((v) => v[1]);
27
+
28
+ /**
29
+ * Recursively iterate through relations with matching prefixes.
30
+ * Attempts to get the accompanying AttributeInfo given a full query.
31
+ * @param full_query
32
+ * @param ass
33
+ */
34
+ const get_association = (
35
+ full_query,
36
+ ass
37
+ ): {
38
+ [association: string]: AttributeInfo;
39
+ } => {
40
+ if (ass instanceof Array) {
41
+ return ass.find((dict) => {
42
+ return full_query.includes(Object.keys(dict)[0]);
43
+ });
44
+ }
45
+
46
+ const key = Object.keys(ass).find((k) => full_query.includes(k));
47
+
48
+ if (typeof key === "undefined") return null;
49
+
50
+ const remainder = full_query.slice(full_query.indexOf(key) + key.length + 1);
51
+
52
+ return get_association(remainder, ass[key]);
53
+ };
54
+
55
+ export default class QueryBuilder {
56
+ attributes = {};
57
+ associations;
58
+ possible_values = [];
59
+
60
+ constructor(attributes, associations) {
61
+ this.attributes = attributes;
62
+ this.associations = associations;
63
+
64
+ this.possible_values = Object.keys(attributes).concat(
65
+ associations.map((v) => Object.keys(v)[0]).flat()
66
+ );
67
+ }
68
+
69
+ parseQueries(queries) {
70
+ let s = queries || {};
71
+
72
+ let f = {};
73
+
74
+ if (Object.entries(s)?.length > 0) {
75
+ for (const [k, v] of Object.entries(s)) {
76
+ // sorting parameter
77
+ if (k === "s") continue;
78
+
79
+ const query = options_values
80
+ .filter((v) => k.includes(v))
81
+ .reduce((x, y) => (x.length > y.length ? x : y), "");
82
+
83
+ const attribute = this.possible_values
84
+ .filter((v) => k.includes(v))
85
+ .reduce((x, y) => (x.length > y.length ? x : y), "");
86
+
87
+ const asses = get_association(
88
+ k,
89
+ this.associations.reduce((acc, cur) => ({ ...acc, ...cur }), {})
90
+ );
91
+
92
+ if (asses) {
93
+ const full_ass_key = Object.keys(asses)[0];
94
+ const full_ass_values = Object.values(asses)[0];
95
+
96
+ const t = {};
97
+ t["query"] = query;
98
+ t["value"] = v;
99
+ t["type"] = full_ass_values?.type;
100
+ t["label"] = full_ass_values?.label;
101
+ t["ass"] = attribute;
102
+
103
+ if (query.length > 0 && attribute.length > 0) {
104
+ if (f[`${attribute}_${full_ass_key}`] instanceof Array) {
105
+ f[`${attribute}_${full_ass_key}`].push(t);
106
+ } else {
107
+ f[`${attribute}_${full_ass_key}`] = [];
108
+ f[`${attribute}_${full_ass_key}`].push(t);
109
+ }
110
+ }
111
+ } else {
112
+ const t = {};
113
+ t["query"] = query;
114
+ t["value"] = v;
115
+ t["type"] = this.attributes[attribute]?.type;
116
+ t["label"] = this.attributes[attribute]?.label;
117
+
118
+ if (query.length > 0 && attribute.length > 0) {
119
+ if (f[`${attribute}`] instanceof Array) {
120
+ f[`${attribute}`].push(t);
121
+ } else {
122
+ f[`${attribute}`] = [];
123
+ f[`${attribute}`].push(t);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ } else {
129
+ return {};
130
+ }
131
+
132
+ return f;
133
+ }
134
+ }
@@ -0,0 +1,5 @@
1
+ export function convertToDate(date) {
2
+ const offset = date.getTimezoneOffset();
3
+ const new_date = new Date(date.getTime() - offset * 60 * 1000);
4
+ return new_date.toISOString().split("T")[0];
5
+ }
@@ -0,0 +1,65 @@
1
+ import { Dropdown } from "bootstrap";
2
+
3
+ export default function (root: Document | HTMLElement) {
4
+ const CLASS_NAME = "has-child-dropdown-show";
5
+ Dropdown.prototype.toggle = (function (_orginal) {
6
+ return function () {
7
+ root.querySelectorAll("." + CLASS_NAME).forEach(function (e) {
8
+ e.classList.remove(CLASS_NAME);
9
+ });
10
+ let dd = this._element
11
+ .closest(".dropdown")
12
+ .parentNode.closest(".dropdown");
13
+ for (; dd && dd !== root; dd = dd.parentNode.closest(".dropdown")) {
14
+ dd.classList.add(CLASS_NAME);
15
+ }
16
+ return _orginal.call(this);
17
+ };
18
+ })(Dropdown.prototype.toggle);
19
+
20
+ root.querySelectorAll(".dropdown").forEach(function (dd) {
21
+ dd.addEventListener("hide.bs.dropdown", function (e) {
22
+ if (this.classList.contains(CLASS_NAME)) {
23
+ this.classList.remove(CLASS_NAME);
24
+ e.preventDefault();
25
+ }
26
+ if (
27
+ e.clickEvent &&
28
+ e.clickEvent
29
+ .composedPath()
30
+ .some(
31
+ (el) => el.classList && el.classList.contains("dropdown-toggle")
32
+ )
33
+ ) {
34
+ e.preventDefault();
35
+ }
36
+ e.stopPropagation(); // do not need pop in multi level mode
37
+ });
38
+ });
39
+
40
+ // for hover
41
+ function getDropdown(element) {
42
+ return Dropdown.getInstance(element) || new Dropdown(element);
43
+ }
44
+
45
+ root
46
+ .querySelectorAll(".dropdown-hover, .dropdown-hover-all .dropdown")
47
+ .forEach(function (dd) {
48
+ dd.addEventListener("mouseenter", function (e) {
49
+ let toggle = e.target.querySelector(
50
+ ':scope>[data-bs-toggle="dropdown"]'
51
+ );
52
+ if (!toggle.classList.contains("show")) {
53
+ getDropdown(toggle).toggle();
54
+ }
55
+ });
56
+ dd.addEventListener("mouseleave", function (e) {
57
+ let toggle = e.target.querySelector(
58
+ ':scope>[data-bs-toggle="dropdown"]'
59
+ );
60
+ if (toggle.classList.contains("show")) {
61
+ getDropdown(toggle).toggle();
62
+ }
63
+ });
64
+ });
65
+ }
@@ -0,0 +1,4 @@
1
+ module RansackSearchElement
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module RansackSearchElement
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module RansackSearchElement
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Ransack search element</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "ransack_search_element/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ RansackSearchElement::Engine.routes.draw do
2
+ end
@@ -0,0 +1,21 @@
1
+ module RansackSearchElement
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace RansackSearchElement
4
+
5
+ config.autoload_once_paths = %W(
6
+ #{root}/app/channels
7
+ #{root}/app/controllers
8
+ #{root}/app/controllers/concerns
9
+ #{root}/app/helpers
10
+ #{root}/app/models
11
+ #{root}/app/models/concerns
12
+ #{root}/app/jobs
13
+ )
14
+
15
+ initializer "ransack_search_element.helpers", before: :load_config_initializers do
16
+ ActiveSupport.on_load(:action_controller_base) do
17
+ helper RansackSearchElement::Engine.helpers
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module RansackSearchElement
2
+ VERSION = '0.0.2-alpha'
3
+ end
@@ -0,0 +1,5 @@
1
+ require "ransack_search_element/version"
2
+ require "ransack_search_element/engine"
3
+
4
+ module RansackSearchElement
5
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :ransack_search_element do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ransack_search_element
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2.pre.alpha
5
+ platform: ruby
6
+ authors:
7
+ - Arthur Dzieniszewski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-09-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.1.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 6.1.4
27
+ description: Generic, advanced search element for Ransack.
28
+ email:
29
+ - arthurdzieniszewski@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - app/assets/config/ransack_search_element_manifest.js
38
+ - app/assets/stylesheets/ransack_search_element/application.scss
39
+ - app/controllers/ransack_search_element/application_controller.rb
40
+ - app/helpers/ransack_search_element/application_helper.rb
41
+ - app/javascript/ransack-search-element/components/FilterDropdown.tsx
42
+ - app/javascript/ransack-search-element/components/FilterRow.tsx
43
+ - app/javascript/ransack-search-element/components/FormInput.tsx
44
+ - app/javascript/ransack-search-element/index.ts
45
+ - app/javascript/ransack-search-element/utilities/QueryBuilder.ts
46
+ - app/javascript/ransack-search-element/utilities/dateConverter.ts
47
+ - app/javascript/ransack-search-element/utilities/registerNestedDropdown.ts
48
+ - app/jobs/ransack_search_element/application_job.rb
49
+ - app/mailers/ransack_search_element/application_mailer.rb
50
+ - app/models/ransack_search_element/application_record.rb
51
+ - app/views/layouts/ransack_search_element/application.html.erb
52
+ - config/routes.rb
53
+ - lib/ransack_search_element.rb
54
+ - lib/ransack_search_element/engine.rb
55
+ - lib/ransack_search_element/version.rb
56
+ - lib/tasks/ransack_search_element_tasks.rake
57
+ homepage: https://github.com/adzienis/ransack-search-element
58
+ licenses:
59
+ - MIT
60
+ metadata:
61
+ allowed_push_host: https://rubygems.org/
62
+ homepage_uri: https://github.com/adzienis/ransack-search-element
63
+ source_code_uri: https://github.com/adzienis/ransack-search-element
64
+ changelog_uri: https://github.com/adzienis/ransack-search-element/CHANGELOG.md
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">"
77
+ - !ruby/object:Gem::Version
78
+ version: 1.3.1
79
+ requirements: []
80
+ rubygems_version: 3.2.15
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Generic, advanced search element for Ransack.
84
+ test_files: []