@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 +2 -2
- package/src/base-model/scope.mjs +12 -0
- package/src/base-model.mjs +36 -0
- package/src/bootstrap/card.jsx +18 -16
- package/src/collection-loader.jsx +16 -9
- package/src/collection.mjs +13 -12
- package/src/super-admin/index.jsx +1 -1
- package/src/table/filters/filter-form.jsx +277 -0
- package/src/table/filters/index.jsx +95 -0
- package/src/table/table.jsx +49 -3
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
]
|
|
17
17
|
},
|
|
18
18
|
"name": "@kaspernj/api-maker",
|
|
19
|
-
"version": "1.0.
|
|
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.
|
|
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
|
+
}
|
package/src/base-model.mjs
CHANGED
|
@@ -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 = []
|
package/src/bootstrap/card.jsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
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={
|
|
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=
|
|
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
|
|
62
|
-
<i className="
|
|
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
|
|
67
|
-
<i className="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/collection.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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)
|
package/src/table/table.jsx
CHANGED
|
@@ -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={
|
|
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
|
-
|
|
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)
|