@kaspernj/api-maker 1.0.229 → 1.0.231

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.
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  ]
17
17
  },
18
18
  "name": "@kaspernj/api-maker",
19
- "version": "1.0.229",
19
+ "version": "1.0.231",
20
20
  "type": "module",
21
21
  "description": "",
22
22
  "main": "index.js",
@@ -73,7 +73,7 @@
73
73
  "eslint-find-rules": "^4.0.0",
74
74
  "eslint-plugin-jest": "^27.0.1",
75
75
  "eslint-plugin-react": "^7.23.2",
76
- "i18n-on-steroids": "^1.0.2",
76
+ "i18n-on-steroids": "^1.0.5",
77
77
  "jest": "^29.0.1",
78
78
  "jsdom": "^20.0.0"
79
79
  }
@@ -0,0 +1,12 @@
1
+ import {digg} from "diggerize"
2
+ import inflection from "inflection"
3
+
4
+ export default class ApiMakerBaseModelScope {
5
+ constructor(scopeData) {
6
+ this.scopeData = scopeData
7
+ }
8
+
9
+ name() {
10
+ return inflection.camelize(digg(this, "scopeData", "name"), true)
11
+ }
12
+ }
@@ -11,6 +11,7 @@ import ModelName from "./model-name.mjs"
11
11
  import NotLoadedError from "./not-loaded-error.mjs"
12
12
  import objectToFormData from "object-to-formdata"
13
13
  import Reflection from "./base-model/reflection.mjs"
14
+ import Scope from "./base-model/scope.mjs"
14
15
  import Services from "./services.mjs"
15
16
  import ValidationError from "./validation-error.mjs"
16
17
  import {ValidationErrors} from "./validation-errors.mjs"
@@ -84,6 +85,41 @@ export default class BaseModel {
84
85
  return new Collection({modelClass: this}, {ransack: query})
85
86
  }
86
87
 
88
+ static ransackableAssociations() {
89
+ const relationships = digg(this.modelClassData(), "ransackable_associations")
90
+ const reflections = []
91
+
92
+ for (const relationshipData of relationships) {
93
+ reflections.push(new Reflection(relationshipData))
94
+ }
95
+
96
+ return reflections
97
+ }
98
+
99
+ static ransackableAttributes() {
100
+ const attributes = digg(this.modelClassData(), "ransackable_attributes")
101
+ const result = []
102
+
103
+ for (const attributeData of attributes) {
104
+ result.push(new Attribute(attributeData))
105
+ }
106
+
107
+ return result
108
+ }
109
+
110
+ static ransackableScopes() {
111
+ const ransackableScopes = digg(this.modelClassData(), "ransackable_scopes")
112
+ const result = []
113
+
114
+ for (const scopeData of ransackableScopes) {
115
+ const scope = new Scope(scopeData)
116
+
117
+ result.push(scope)
118
+ }
119
+
120
+ return result
121
+ }
122
+
87
123
  static reflections() {
88
124
  const relationships = digg(this.modelClassData(), "relationships")
89
125
  const reflections = []
@@ -1,4 +1,5 @@
1
- import {digs} from "diggerize"
1
+ import classNames from "classnames"
2
+ import {digg, digs} from "diggerize"
2
3
  import PropTypes from "prop-types"
3
4
  import React from "react"
4
5
 
@@ -18,6 +19,7 @@ export default class ApiMakerBootstrapCard extends React.PureComponent {
18
19
  defaultExpanded: PropTypes.bool.isRequired,
19
20
  expandable: PropTypes.bool.isRequired,
20
21
  expandableHide: PropTypes.bool.isRequired,
22
+ footer: PropTypes.node,
21
23
  header: PropTypes.node,
22
24
  striped: PropTypes.bool,
23
25
  responsiveTable: PropTypes.bool.isRequired,
@@ -39,6 +41,7 @@ export default class ApiMakerBootstrapCard extends React.PureComponent {
39
41
  defaultExpanded,
40
42
  expandable,
41
43
  expandableHide,
44
+ footer,
42
45
  header,
43
46
  responsiveTable,
44
47
  striped,
@@ -46,11 +49,14 @@ export default class ApiMakerBootstrapCard extends React.PureComponent {
46
49
  ...restProps
47
50
  } = this.props
48
51
  const {expanded} = digs(this.state, "expanded")
52
+ const cardHeaderStyle = {display: "flex"}
53
+
54
+ if (!expanded) cardHeaderStyle["borderBottom"] = "0"
49
55
 
50
56
  return (
51
- <div className={this.classNames()} ref="card" {...restProps}>
57
+ <div className={classNames("component-bootstrap-card", "card", "card-default", className)} data-has-footer={Boolean(footer)} ref="card" {...restProps}>
52
58
  {(controls || expandable || header) &&
53
- <div className={`card-header d-flex ${!expanded && "border-bottom-0"}`}>
59
+ <div className="card-header" style={cardHeaderStyle}>
54
60
  <div style={{alignSelf: "center", marginRight: "auto"}}>
55
61
  {header}
56
62
  </div>
@@ -58,13 +64,13 @@ export default class ApiMakerBootstrapCard extends React.PureComponent {
58
64
  <div style={{alignSelf: "center"}}>
59
65
  {controls}
60
66
  {expandable && expanded &&
61
- <a className="collapse-card-button text-muted" href="#" onClick={this.onCollapseClicked}>
62
- <i className="la la-angle-up" />
67
+ <a className="collapse-card-button text-muted" href="#" onClick={digg(this, "onCollapseClicked")}>
68
+ <i className="fa fa-angle-up" />
63
69
  </a>
64
70
  }
65
71
  {expandable && !expanded &&
66
- <a className="expand-card-button text-muted" href="#" onClick={this.onExpandClicked}>
67
- <i className="la la-angle-down" />
72
+ <a className="expand-card-button text-muted" href="#" onClick={digg(this, "onExpandClicked")}>
73
+ <i className="fa fa-angle-down" />
68
74
  </a>
69
75
  }
70
76
  </div>
@@ -81,19 +87,15 @@ export default class ApiMakerBootstrapCard extends React.PureComponent {
81
87
  {!table && children}
82
88
  </div>
83
89
  }
90
+ {footer &&
91
+ <div className="card-footer">
92
+ {footer}
93
+ </div>
94
+ }
84
95
  </div>
85
96
  )
86
97
  }
87
98
 
88
- classNames () {
89
- const classNames = ["component-bootstrap-card", "card", "card-default"]
90
-
91
- if (this.props.className)
92
- classNames.push(this.props.className)
93
-
94
- return classNames.join(" ")
95
- }
96
-
97
99
  bodyClassNames () {
98
100
  const {expandableHide, responsiveTable, table} = digs(this.props, "expandableHide", "responsiveTable", "table")
99
101
  const {expanded} = digs(this.state, "expanded")
@@ -4,7 +4,6 @@ import {digg, digs} from "diggerize"
4
4
  import EventCreated from "./event-created"
5
5
  import EventDestroyed from "./event-destroyed"
6
6
  import EventUpdated from "./event-updated"
7
- import instanceOfClassName from "./instance-of-class-name"
8
7
  import {LocationChanged} from "on-location-changed/src/location-changed-component"
9
8
  import Params from "./params"
10
9
  import PropTypes from "prop-types"
@@ -54,9 +53,11 @@ export default class CollectionLoader extends React.PureComponent {
54
53
  query: undefined,
55
54
  queryName,
56
55
  queryQName: `${queryName}_q`,
56
+ querySName: `${queryName}_s`,
57
57
  queryPageName: `${queryName}_page`,
58
58
  qParams: undefined,
59
59
  result: undefined,
60
+ searchParams: undefined,
60
61
  showNoRecordsAvailableContent: false,
61
62
  showNoRecordsFoundContent: false
62
63
  })
@@ -83,17 +84,26 @@ export default class CollectionLoader extends React.PureComponent {
83
84
  }
84
85
 
85
86
  loadQParams () {
86
- const {queryQName} = digs(this.shape, "queryQName")
87
+ const {queryQName, querySName} = digs(this.shape, "queryQName", "querySName")
87
88
  const params = Params.parse()
88
89
  const qParams = Object.assign({}, this.props.defaultParams, params[queryQName])
90
+ const searchParams = []
89
91
 
90
- this.shape.set({qParams})
92
+ if (params[querySName]) {
93
+ for (const rawSearchParam of params[querySName]) {
94
+ const parsedSearchParam = JSON.parse(rawSearchParam)
95
+
96
+ searchParams.push(parsedSearchParam)
97
+ }
98
+ }
99
+
100
+ this.shape.set({qParams, searchParams})
91
101
  }
92
102
 
93
103
  loadModels = async () => {
94
104
  const params = Params.parse()
95
105
  const {abilities, collection, groupBy, modelClass, onModelsLoaded, preloads, select, selectColumns} = this.props
96
- const {qParams, queryPageName, queryQName} = digs(this.shape, "qParams", "queryPageName", "queryQName")
106
+ const {qParams, queryPageName, queryQName, searchParams} = digs(this.shape, "qParams", "queryPageName", "queryQName", "searchParams")
97
107
 
98
108
  let query = collection?.clone() || modelClass.ransack()
99
109
 
@@ -101,6 +111,7 @@ export default class CollectionLoader extends React.PureComponent {
101
111
 
102
112
  query = query
103
113
  .ransack(qParams)
114
+ .search(searchParams)
104
115
  .searchKey(queryQName)
105
116
  .page(params[queryPageName])
106
117
  .pageKey(queryPageName)
@@ -151,11 +162,7 @@ export default class CollectionLoader extends React.PureComponent {
151
162
  }
152
163
 
153
164
  onLocationChanged = () => {
154
- const {queryQName} = digs(this.shape, "queryQName")
155
- const params = Params.parse()
156
- const qParams = Object.assign({}, this.props.defaultParams, params[queryQName])
157
-
158
- this.shape.set({qParams})
165
+ this.loadQParams()
159
166
  this.loadModels()
160
167
  }
161
168
 
@@ -6,7 +6,9 @@ import {incorporate} from "incorporator"
6
6
  import modelClassRequire from "./model-class-require.mjs"
7
7
  import Result from "./result.mjs"
8
8
 
9
- class ApiMakerCollection {
9
+ export default class ApiMakerCollection {
10
+ static apiMakerType = "Collection"
11
+
10
12
  constructor (args, queryArgs = {}) {
11
13
  this.queryArgs = queryArgs
12
14
  this.args = args
@@ -122,6 +124,7 @@ class ApiMakerCollection {
122
124
  if (this.queryArgs.preload) params.preload = this.queryArgs.preload
123
125
  if (this.queryArgs.page) params.page = this.queryArgs.page
124
126
  if (this.queryArgs.per) params.per = this.queryArgs.per
127
+ if (this.queryArgs.search) params.search = this.queryArgs.search
125
128
  if (this.queryArgs.select) params.select = this.queryArgs.select
126
129
  if (this.queryArgs.selectColumns) params.select_columns = this.queryArgs.selectColumns
127
130
 
@@ -137,10 +140,7 @@ class ApiMakerCollection {
137
140
  }
138
141
 
139
142
  ransack (params) {
140
- if (params) {
141
- this._merge({ransack: params})
142
- }
143
-
143
+ if (params) this._merge({ransack: params})
144
144
  return this
145
145
  }
146
146
 
@@ -155,11 +155,16 @@ class ApiMakerCollection {
155
155
  return result
156
156
  }
157
157
 
158
- searchKey (searchKey) {
158
+ search(params) {
159
+ if (params) this._merge({search: params})
160
+ return this
161
+ }
162
+
163
+ searchKey(searchKey) {
159
164
  return this._merge({searchKey})
160
165
  }
161
166
 
162
- select (originalSelect) {
167
+ select(originalSelect) {
163
168
  const newSelect = {}
164
169
 
165
170
  for (const originalModelName in originalSelect) {
@@ -178,7 +183,7 @@ class ApiMakerCollection {
178
183
  return this._merge({select: newSelect})
179
184
  }
180
185
 
181
- selectColumns (originalSelect) {
186
+ selectColumns(originalSelect) {
182
187
  const newSelect = {}
183
188
 
184
189
  for (const originalModelName in originalSelect) {
@@ -249,7 +254,3 @@ class ApiMakerCollection {
249
254
  )
250
255
  }
251
256
  }
252
-
253
- ApiMakerCollection.apiMakerType = "Collection"
254
-
255
- export default ApiMakerCollection
@@ -22,7 +22,7 @@ class ApiMakerSuperAdmin extends React.PureComponent {
22
22
  if (queryParams.model) modelClass = modelsModule[queryParams.model]
23
23
 
24
24
  return (
25
- <Layout headerTitle={modelClass?.modelName()?.human({count: 2})}>
25
+ <Layout active={queryParams.model} headerTitle={modelClass?.modelName()?.human({count: 2})}>
26
26
  {pageToShow == "index" &&
27
27
  <IndexPage
28
28
  currentUser={currentUser}
@@ -0,0 +1,277 @@
1
+ import Attribute from "../../base-model/attribute"
2
+ import {digs} from "diggerize"
3
+ import Input from "../../inputs/input"
4
+ import PropTypes from "prop-types"
5
+ import PropTypesExact from "prop-types-exact"
6
+ import React from "react"
7
+ import Reflection from "../../base-model/reflection"
8
+ import Select from "../../inputs/select"
9
+ import Services from "../../services.mjs"
10
+ import Shape from "set-state-compare/src/shape"
11
+
12
+ class AttributeElement extends React.PureComponent {
13
+ static propTypes = {
14
+ active: PropTypes.bool.isRequired,
15
+ attribute: PropTypes.instanceOf(Attribute).isRequired,
16
+ currentModelClass: PropTypes.func.isRequired,
17
+ fikter: PropTypes.object,
18
+ onClick: PropTypes.func.isRequired
19
+ }
20
+
21
+ render() {
22
+ const {active, attribute, currentModelClass} = digs(this.props, "active", "attribute", "currentModelClass")
23
+ const style = {}
24
+
25
+ if (active) style.fontWeight = "bold"
26
+
27
+ return (
28
+ <div onClick={digg(this, "onAttributeClicked")} style={style}>
29
+ {currentModelClass.humanAttributeName(inflection.camelize(attribute.name(), true))}
30
+ </div>
31
+ )
32
+ }
33
+
34
+ onAttributeClicked = (e) => {
35
+ e.preventDefault()
36
+
37
+ this.props.onClick({attribute: digg(this, "props", "attribute")})
38
+ }
39
+ }
40
+
41
+ class ReflectionElement extends React.PureComponent {
42
+ static propTypes = {
43
+ currentModelClass: PropTypes.func.isRequired,
44
+ onClick: PropTypes.func.isRequired,
45
+ reflection: PropTypes.instanceOf(Reflection).isRequired
46
+ }
47
+
48
+ render() {
49
+ const {currentModelClass, reflection} = digs(this.props, "currentModelClass", "reflection")
50
+
51
+ return (
52
+ <div key={reflection.name()} onClick={digg(this, "onReflectionClicked")}>
53
+ {currentModelClass.humanAttributeName(reflection.name())}
54
+ </div>
55
+ )
56
+ }
57
+
58
+ onReflectionClicked = (e) => {
59
+ e.preventDefault()
60
+
61
+ this.props.onClick({reflection: digg(this, "props", "reflection")})
62
+ }
63
+ }
64
+
65
+ export default class ApiMakerTableFiltersRelationshipSelect extends React.PureComponent {
66
+ static propTypes = PropTypesExact({
67
+ filter: PropTypes.object,
68
+ modelClass: PropTypes.func.isRequired,
69
+ querySearchName: PropTypes.string.isRequired
70
+ })
71
+
72
+ shape = new Shape(this, {
73
+ attribute: this.currentModelClassFromPath(this.props.filter.p || []).ransackableAttributes().find((attribute) => attribute.name() == this.props.filter.a),
74
+ path: this.props.filter.p || [],
75
+ predicate: undefined,
76
+ predicates: undefined,
77
+ value: this.props.filter.v
78
+ })
79
+ valueInputRef = React.createRef()
80
+
81
+ componentDidMount() {
82
+ this.loadRansackPredicates()
83
+ }
84
+
85
+ async loadRansackPredicates() {
86
+ const response = await Services.current().sendRequest("Ransack::Predicates")
87
+ const predicates = digg(response, "predicates")
88
+ let currentPredicate
89
+
90
+ if (this.props.filter.pre) {
91
+ currentPredicate = predicates.find((predicate) => predicate.name == this.props.filter.pre)
92
+ }
93
+
94
+ this.shape.set({
95
+ predicate: currentPredicate,
96
+ predicates
97
+ })
98
+ }
99
+
100
+ render() {
101
+ const {valueInputRef} = digs(this, "valueInputRef")
102
+ const currentModelClass = this.currentModelClass()
103
+ const {attribute, predicate, predicates, value} = digs(this.shape, "attribute", "predicate", "predicates", "value")
104
+
105
+ return (
106
+ <div className="api-maker--table--filters--relationship-select">
107
+ <form onSubmit={digg(this, "onSubmit")}>
108
+ <div>
109
+ {this.currentPathParts().map(({translation}, pathPartIndex) =>
110
+ <span key={`${pathPartIndex}-${translation}`}>
111
+ {pathPartIndex > 0 &&
112
+ <span style={{marginRight: "5px", marginLeft: "5px"}}>
113
+ -
114
+ </span>
115
+ }
116
+ {translation}
117
+ </span>
118
+ )}
119
+ </div>
120
+ <div style={{display: "flex"}}>
121
+ <div>
122
+ {this.sortedByName(this.reflectionsWithModelClass(currentModelClass.ransackableAssociations()), currentModelClass).map((reflection) =>
123
+ <ReflectionElement
124
+ currentModelClass={currentModelClass}
125
+ key={reflection.name()}
126
+ onClick={digg(this, "onReflectionClicked")}
127
+ reflection={reflection}
128
+ />
129
+ )}
130
+ </div>
131
+ <div>
132
+ {this.sortedByName(currentModelClass.ransackableAttributes(), currentModelClass).map((attribute) =>
133
+ <AttributeElement
134
+ active={attribute.name() == this.shape.attribute?.name()}
135
+ attribute={attribute}
136
+ currentModelClass={currentModelClass}
137
+ key={attribute.name()}
138
+ onClick={digg(this, "onAttributeClicked")}
139
+ />
140
+ )}
141
+ {currentModelClass.ransackableScopes().map((scope) =>
142
+ <div>
143
+ {scope.name()}
144
+ </div>
145
+ )}
146
+ </div>
147
+ <div>
148
+ {predicates &&
149
+ <Select
150
+ defaultValue={predicate?.name}
151
+ includeBlank
152
+ onChange={digg(this, "onPredicateChanged")}
153
+ options={predicates.map((predicate) => digg(predicate, "name"))}
154
+ />
155
+ }
156
+ </div>
157
+ <div>
158
+ {attribute && predicate &&
159
+ <Input defaultValue={value} inputRef={valueInputRef} />
160
+ }
161
+ </div>
162
+ </div>
163
+ <div>
164
+ <Button disabled={!attribute || !predicate}>
165
+ {I18n.t("js.api_maker.table.filters.relationship_select.apply", {defaultValue: "Apply"})}
166
+ </Button>
167
+ </div>
168
+ </form>
169
+ </div>
170
+ )
171
+ }
172
+
173
+ currentModelClass() {
174
+ const {path} = digs(this.shape, "path")
175
+
176
+ return this.currentModelClassFromPath(path)
177
+ }
178
+
179
+ currentModelClassFromPath(path) {
180
+ const {modelClass} = digs(this.props, "modelClass")
181
+ let currentModelClass = modelClass
182
+
183
+ for (const pathPart of path) {
184
+ currentModelClass = currentModelClass.ransackableAssociations().find((reflection) => reflection.name() == pathPart).modelClass()
185
+ }
186
+
187
+ return currentModelClass
188
+ }
189
+
190
+ currentPathParts() {
191
+ const {modelClass} = digs(this.props, "modelClass")
192
+ const {path} = digs(this.shape, "path")
193
+ const result = []
194
+ let currentModelClass = modelClass
195
+
196
+ result.push({
197
+ modelClass,
198
+ translation: modelClass.modelName().human({count: 2})
199
+ })
200
+
201
+ for (const pathPart of path) {
202
+ const pathPartTranslation = currentModelClass.humanAttributeName(pathPart)
203
+
204
+ currentModelClass = currentModelClass.ransackableAssociations().find((reflection) => reflection.name() == pathPart).modelClass()
205
+
206
+ result.push({
207
+ modelClass: currentModelClass,
208
+ translation: pathPartTranslation
209
+ })
210
+ }
211
+
212
+ return result
213
+ }
214
+
215
+ onAttributeClicked = ({attribute}) => {
216
+ this.shape.set({attribute})
217
+ }
218
+
219
+ onPredicateChanged = (e) => {
220
+ const chosenPredicateName = digg(e, "target", "value")
221
+ const predicate = this.shape.predicates.find((predicate) => predicate.name == chosenPredicateName)
222
+
223
+ this.shape.set({predicate})
224
+ }
225
+
226
+ onReflectionClicked = ({reflection}) => {
227
+ const newPath = this.shape.path.concat([reflection.name()])
228
+
229
+ this.shape.set({
230
+ attribute: undefined,
231
+ path: newPath
232
+ })
233
+
234
+ this.props.onPathChanged
235
+ }
236
+
237
+ onSubmit = (e) => {
238
+ e.preventDefault()
239
+
240
+ const {filter, querySearchName} = digs(this.props, "filter", "querySearchName")
241
+ const {attribute, path, predicate} = digs(this.shape, "attribute", "path", "predicate")
242
+ const {filterIndex} = digs(filter, "filterIndex")
243
+ const searchParams = Params.parse()[querySearchName] || {}
244
+ const value = digg(this, "valueInputRef", "current", "value")
245
+
246
+ searchParams[filterIndex] = JSON.stringify({
247
+ a: attribute.name(),
248
+ p: path,
249
+ pre: digg(predicate, "name"),
250
+ v: value
251
+ })
252
+
253
+ const newParams = {}
254
+
255
+ newParams[querySearchName] = searchParams
256
+
257
+ Params.changeParams(newParams)
258
+ }
259
+
260
+ reflectionsWithModelClass(reflections) {
261
+ return reflections.filter((reflection) => {
262
+ try {
263
+ reflection.modelClass()
264
+
265
+ return true
266
+ } catch (error) {
267
+ return false
268
+ }
269
+ })
270
+ }
271
+
272
+ sortedByName(reflections, currentModelClass) {
273
+ return reflections.sort((a, b) =>
274
+ currentModelClass.humanAttributeName(a.name()).toLowerCase().localeCompare(currentModelClass.humanAttributeName(b.name()).toLowerCase())
275
+ )
276
+ }
277
+ }
@@ -0,0 +1,95 @@
1
+ import PropTypes from "prop-types"
2
+ import React from "react"
3
+ import FilterForm from "./filter-form"
4
+ import Shape from "set-state-compare/src/shape"
5
+ import withQueryParams from "on-location-changed/src/with-query-params"
6
+
7
+ class ApiMakerTableFilter extends React.PureComponent {
8
+ static propTypes = {
9
+ a: PropTypes.string.isRequired,
10
+ filterIndex: PropTypes.number.isRequired,
11
+ onClick: PropTypes.func.isRequired,
12
+ p: PropTypes.array.isRequired,
13
+ pre: PropTypes.string.isRequired,
14
+ v: PropTypes.string.isRequired
15
+ }
16
+
17
+ render() {
18
+ const {a, p, pre, v} = digs(this.props, "a", "p", "pre", "v")
19
+
20
+ return (
21
+ <div onClick={digg(this, "onFilterClicked")} style={{display: "inline-block", backgroundColor: "grey", padding: "10px 6px"}}>
22
+ {p.join(".")}.{a} {pre} {v}
23
+ </div>
24
+ )
25
+ }
26
+
27
+ onFilterClicked = (e) => {
28
+ e.preventDefault()
29
+
30
+ const {a, filterIndex, p, pre, v} = digs(this.props, "a", "filterIndex", "p", "pre", "v")
31
+
32
+ this.props.onClick({a, filterIndex, p, pre, v})
33
+ }
34
+ }
35
+
36
+ class ApiMakerTableFilters extends React.PureComponent {
37
+ static propTypes = {
38
+ modelClass: PropTypes.func.isRequired,
39
+ queryName: PropTypes.string.isRequired,
40
+ queryParams: PropTypes.object.isRequired
41
+ }
42
+
43
+ shape = new Shape(this, {
44
+ filter: undefined
45
+ })
46
+
47
+ render() {
48
+ const {modelClass} = this.props
49
+ const {filter} = digs(this.shape, "filter")
50
+ const currentFilters = this.currentFilters()
51
+
52
+ return (
53
+ <div className="api-maker--table--filters--edit">
54
+ <button onClick={digg(this, "onAddFilterClicked")}>
55
+ {I18n.t("js.api_maker.table.filters.add_new_filter", {defaultValue: "Add new filter"})}
56
+ </button>
57
+ {filter &&
58
+ <FilterForm
59
+ filter={filter}
60
+ key={`filter-${filter.filterIndex}`}
61
+ modelClass={modelClass}
62
+ querySearchName={this.querySearchName()}
63
+ />
64
+ }
65
+ {currentFilters?.map((filterData, filterIndex) =>
66
+ <ApiMakerTableFilter key={filterIndex} filterIndex={filterIndex} onClick={digg(this, "onFilterClicked")} {...JSON.parse(filterData)} />
67
+ )}
68
+ </div>
69
+ )
70
+ }
71
+
72
+ currentFilters() {
73
+ const {queryParams} = this.props
74
+ const currentFilters = queryParams[this.querySearchName()] || []
75
+
76
+ return currentFilters
77
+ }
78
+
79
+ onAddFilterClicked = (e) => {
80
+ e.preventDefault()
81
+
82
+ const newFilterIndex = this.currentFilters().length
83
+
84
+ this.shape.set({
85
+ filter: {
86
+ filterIndex: newFilterIndex
87
+ }
88
+ })
89
+ }
90
+
91
+ onFilterClicked = (args) => this.shape.set({filter: args})
92
+ querySearchName = () => `${this.props.queryName}_s`
93
+ }
94
+
95
+ export default withQueryParams(ApiMakerTableFilters)
@@ -6,8 +6,8 @@ import CollectionLoader from "../collection-loader"
6
6
  import columnVisible from "./column-visible.mjs"
7
7
  import {debounce} from "debounce"
8
8
  import {digg, digs} from "diggerize"
9
+ import Filters from "./filters"
9
10
  import inflection from "inflection"
10
- import instanceOfClassName from "../instance-of-class-name"
11
11
  import modelClassRequire from "../model-class-require.mjs"
12
12
  import ModelRow from "./model-row"
13
13
  import Paginate from "../bootstrap/paginate"
@@ -94,6 +94,7 @@ class ApiMakerTable extends React.PureComponent {
94
94
  queryPageName: `${queryName}_page`,
95
95
  qParams: undefined,
96
96
  result: undefined,
97
+ showFilters: false,
97
98
  showNoRecordsAvailableContent: false,
98
99
  showNoRecordsFoundContent: false
99
100
  })
@@ -137,8 +138,10 @@ class ApiMakerTable extends React.PureComponent {
137
138
  preload,
138
139
  qParams,
139
140
  query,
141
+ queryName,
140
142
  result,
141
143
  models,
144
+ showFilters,
142
145
  showNoRecordsAvailableContent,
143
146
  showNoRecordsFoundContent
144
147
  } = digs(
@@ -147,8 +150,10 @@ class ApiMakerTable extends React.PureComponent {
147
150
  "preload",
148
151
  "qParams",
149
152
  "query",
153
+ "queryName",
150
154
  "result",
151
155
  "models",
156
+ "showFilters",
152
157
  "showNoRecordsAvailableContent",
153
158
  "showNoRecordsFoundContent"
154
159
  )
@@ -179,6 +184,9 @@ class ApiMakerTable extends React.PureComponent {
179
184
  {noRecordsFoundContent({models, qParams, overallCount})}
180
185
  </div>
181
186
  }
187
+ {showFilters &&
188
+ <Filters modelClass={modelClass} queryName={queryName} />
189
+ }
182
190
  {qParams && query && result && models && !showNoRecordsAvailableContent && !showNoRecordsFoundContent &&
183
191
  this.cardOrTable()
184
192
  }
@@ -271,6 +279,8 @@ class ApiMakerTable extends React.PureComponent {
271
279
  controlsContent = controls({models, qParams, query, result})
272
280
  }
273
281
 
282
+ controlsContent += this.tableControls()
283
+
274
284
  if (typeof header == "function") {
275
285
  headerContent = header({models, qParams, query, result})
276
286
  } else if (header) {
@@ -300,7 +310,7 @@ class ApiMakerTable extends React.PureComponent {
300
310
  this.filterForm()
301
311
  }
302
312
  {card &&
303
- <Card className={classNames("mb-4", className)} controls={controlsContent} header={headerContent} table={!this.isSmallScreen()} {...restProps}>
313
+ <Card className={classNames("mb-4", className)} controls={this.tableControls()} header={headerContent} footer={this.tableFooter()} table={!this.isSmallScreen()} {...restProps}>
304
314
  {this.tableContent()}
305
315
  </Card>
306
316
  }
@@ -343,6 +353,24 @@ class ApiMakerTable extends React.PureComponent {
343
353
  )
344
354
  }
345
355
 
356
+ tableControls() {
357
+ const {controls} = this.props
358
+
359
+ return (
360
+ <>
361
+ {controls && controls({models, qParams, query, result})}
362
+ <a href="#" onClick={digg(this, "onNewFilterClick")}>
363
+ <i className="fa fa-fw fa-magnifying-glass" />
364
+ </a>
365
+ </>
366
+ )
367
+ }
368
+
369
+ onNewFilterClick = (e) => {
370
+ e.preventDefault()
371
+ this.shape.set({showFilters: !this.shape.showFilters})
372
+ }
373
+
346
374
  tableContent () {
347
375
  const {breakPoint} = digs(this.props, "breakPoint")
348
376
  const {models, preparedColumns} = digs(this.shape, "models", "preparedColumns")
@@ -384,7 +412,25 @@ class ApiMakerTable extends React.PureComponent {
384
412
  )
385
413
  }
386
414
 
387
- className () {
415
+ tableFooter() {
416
+ const {result} = digs(this.shape, "result")
417
+ const currentPage = result.currentPage()
418
+ const totalCount = result.totalCount()
419
+ const perPage = result.perPage()
420
+ const to = Math.min(currentPage * perPage, totalCount)
421
+ const defaultValue = "Showing %{from} to %{to} out of %{total_count} total"
422
+ let from = ((currentPage - 1) * perPage) + 1
423
+
424
+ if (to === 0) from = 0
425
+
426
+ return (
427
+ <div style={{marginTop: "10px"}}>
428
+ {I18n.t("js.api_maker.table.showing_from_to_out_of_total", {defaultValue, from, to, total_count: totalCount})}
429
+ </div>
430
+ )
431
+ }
432
+
433
+ className() {
388
434
  const classNames = ["component-api-maker-live-table"]
389
435
 
390
436
  if (this.props.className)