@kaspernj/api-maker 1.0.214 → 1.0.217

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.
Files changed (48) hide show
  1. package/package.json +5 -4
  2. package/src/base-model.mjs +1 -1
  3. package/src/bootstrap/attribute-row/basic-style.scss +9 -0
  4. package/src/bootstrap/attribute-row/index.jsx +84 -0
  5. package/src/bootstrap/attribute-rows.jsx +27 -0
  6. package/src/bootstrap/card.jsx +135 -0
  7. package/src/bootstrap/checkbox.jsx +79 -0
  8. package/src/bootstrap/checkboxes.jsx +122 -0
  9. package/src/bootstrap/index.js +0 -0
  10. package/src/bootstrap/input.jsx +160 -0
  11. package/src/bootstrap/invalid-feedback.jsx +31 -0
  12. package/src/bootstrap/live-table/model-row.jsx +150 -0
  13. package/src/bootstrap/live-table.jsx +399 -0
  14. package/src/bootstrap/paginate.jsx +153 -0
  15. package/src/bootstrap/radio-buttons.jsx +87 -0
  16. package/src/bootstrap/select.jsx +110 -0
  17. package/src/bootstrap/sort-link.jsx +102 -0
  18. package/src/collection-loader.jsx +7 -8
  19. package/src/inputs/auto-submit.mjs +37 -0
  20. package/src/inputs/checkbox.jsx +97 -0
  21. package/src/inputs/checkboxes.jsx +113 -0
  22. package/src/inputs/id-for-component.mjs +15 -0
  23. package/src/inputs/input-wrapper.jsx +170 -0
  24. package/src/inputs/input.jsx +235 -0
  25. package/src/inputs/money.jsx +177 -0
  26. package/src/inputs/name-for-component.mjs +15 -0
  27. package/src/inputs/select.jsx +87 -0
  28. package/src/model-class-require.mjs +1 -1
  29. package/src/model-name.mjs +1 -1
  30. package/src/super-admin/index.jsx +11 -0
  31. package/src/super-admin/layout/header/index.jsx +60 -0
  32. package/src/super-admin/layout/header/style.scss +124 -0
  33. package/src/super-admin/layout/index.jsx +156 -0
  34. package/src/super-admin/layout/menu/index.jsx +116 -0
  35. package/src/super-admin/layout/menu/menu-content.jsx +106 -0
  36. package/src/super-admin/layout/menu/menu-item/index.jsx +27 -0
  37. package/src/super-admin/layout/menu/menu-item/style.scss +30 -0
  38. package/src/super-admin/layout/menu/style.scss +103 -0
  39. package/src/super-admin/layout/style.scss +25 -0
  40. package/src/table/column-identifier.mjs +23 -0
  41. package/src/table/column-visible.mjs +7 -0
  42. package/src/table/model-row.jsx +182 -0
  43. package/src/table/select-calculator.mjs +48 -0
  44. package/src/table/style.scss +72 -0
  45. package/src/table/table-settings.js +175 -0
  46. package/src/table/table.jsx +498 -0
  47. package/src/table/variables.scss +11 -0
  48. package/src/table/with-breakpoint.jsx +48 -0
@@ -0,0 +1,498 @@
1
+ import "./style"
2
+ import Card from "../bootstrap/card"
3
+ import classNames from "classnames"
4
+ import Collection from "../collection"
5
+ import CollectionLoader from "../collection-loader"
6
+ import columnVisible from "./column-visible.mjs"
7
+ import {debounce} from "debounce"
8
+ import {digg, digs} from "diggerize"
9
+ import inflection from "inflection"
10
+ import instanceOfClassName from "../instance-of-class-name"
11
+ import modelClassRequire from "../model-class-require.mjs"
12
+ import ModelRow from "./model-row"
13
+ import Paginate from "../bootstrap/paginate"
14
+ import Params from "../params"
15
+ import PropTypes from "prop-types"
16
+ import React from "react"
17
+ import selectCalculator from "./select-calculator"
18
+ import Shape from "set-state-compare/src/shape"
19
+ import SortLink from "../bootstrap/sort-link"
20
+ import TableSettings from "./table-settings"
21
+ import uniqunize from "uniqunize"
22
+ import withBreakpoint from "./with-breakpoint"
23
+
24
+ class ApiMakerTable extends React.PureComponent {
25
+ static defaultProps = {
26
+ card: true,
27
+ destroyEnabled: true,
28
+ filterCard: true,
29
+ filterSubmitButton: true,
30
+ noRecordsAvailableContent: undefined,
31
+ noRecordsFoundContent: undefined,
32
+ preloads: [],
33
+ select: {}
34
+ }
35
+
36
+ static propTypes = {
37
+ abilities: PropTypes.object,
38
+ actionsContent: PropTypes.func,
39
+ appHistory: PropTypes.object,
40
+ card: PropTypes.bool.isRequired,
41
+ className: PropTypes.string,
42
+ collection: PropTypes.oneOfType([
43
+ instanceOfClassName("ApiMakerCollection"),
44
+ PropTypes.instanceOf(Collection)
45
+ ]),
46
+ columns: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
47
+ columnsContent: PropTypes.func,
48
+ controls: PropTypes.func,
49
+ currentUser: PropTypes.object,
50
+ defaultDateFormatName: PropTypes.string,
51
+ defaultDateTimeFormatName: PropTypes.string,
52
+ defaultParams: PropTypes.object,
53
+ destroyEnabled: PropTypes.bool.isRequired,
54
+ destroyMessage: PropTypes.string,
55
+ editModelPath: PropTypes.func,
56
+ filterCard: PropTypes.bool.isRequired,
57
+ filterContent: PropTypes.func,
58
+ filterSubmitLabel: PropTypes.node,
59
+ groupBy: PropTypes.array,
60
+ header: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
61
+ identifier: PropTypes.string,
62
+ modelClass: PropTypes.func.isRequired,
63
+ noRecordsAvailableContent: PropTypes.func,
64
+ noRecordsFoundContent: PropTypes.func,
65
+ onModelsLoaded: PropTypes.func,
66
+ paginateContent: PropTypes.func,
67
+ paginationComponent: PropTypes.func,
68
+ preloads: PropTypes.array.isRequired,
69
+ queryName: PropTypes.string,
70
+ select: PropTypes.object,
71
+ selectColumns: PropTypes.object,
72
+ viewModelPath: PropTypes.func
73
+ }
74
+
75
+ filterFormRef = React.createRef()
76
+
77
+ constructor (props) {
78
+ super(props)
79
+
80
+ const collectionKey = digg(props.modelClass.modelClassData(), "collectionKey")
81
+ let queryName = props.queryName
82
+
83
+ if (!queryName) queryName = collectionKey
84
+
85
+ const columnsAsArray = this.columnsAsArray()
86
+
87
+ this.shape = new Shape(this, {
88
+ columns: columnsAsArray,
89
+ identifier: this.props.identifier || `${collectionKey}-default`,
90
+ models: undefined,
91
+ overallCount: undefined,
92
+ preload: undefined,
93
+ preparedColumns: undefined,
94
+ query: undefined,
95
+ queryName,
96
+ queryQName: `${queryName}_q`,
97
+ queryPageName: `${queryName}_page`,
98
+ qParams: undefined,
99
+ result: undefined,
100
+ showNoRecordsAvailableContent: false,
101
+ showNoRecordsFoundContent: false
102
+ })
103
+
104
+ this.loadTableSetting()
105
+ }
106
+
107
+ async loadTableSetting() {
108
+ this.tableSettings = new TableSettings({table: this})
109
+
110
+ const tableSetting = await this.tableSettings.loadExistingOrCreateTableSettings()
111
+ const {columns, preload} = this.tableSettings.preparedColumns(tableSetting)
112
+
113
+ this.shape.set({
114
+ preparedColumns: columns,
115
+ preload: this.mergedPreloads(preload)
116
+ })
117
+ }
118
+
119
+ columnsAsArray = () => {
120
+ if (typeof this.props.columns == "function") return this.props.columns()
121
+
122
+ return this.props.columns
123
+ }
124
+
125
+ mergedPreloads(preload) {
126
+ const {preloads} = this.props
127
+ let mergedPreloads = []
128
+
129
+ if (preloads) mergedPreloads = mergedPreloads.concat(preloads)
130
+ if (preload) mergedPreloads = mergedPreloads.concat(preload)
131
+
132
+ return uniqunize(mergedPreloads)
133
+ }
134
+
135
+ render () {
136
+ const {modelClass, noRecordsAvailableContent, noRecordsFoundContent} = digs(this.props, "modelClass", "noRecordsAvailableContent", "noRecordsFoundContent")
137
+ const {collection, defaultParams, select, selectColumns} = this.props
138
+ const {
139
+ overallCount,
140
+ preload,
141
+ qParams,
142
+ query,
143
+ result,
144
+ models,
145
+ showNoRecordsAvailableContent,
146
+ showNoRecordsFoundContent
147
+ } = digs(
148
+ this.shape,
149
+ "overallCount",
150
+ "preload",
151
+ "qParams",
152
+ "query",
153
+ "result",
154
+ "models",
155
+ "showNoRecordsAvailableContent",
156
+ "showNoRecordsFoundContent"
157
+ )
158
+
159
+ return (
160
+ <div className={this.className()}>
161
+ {preload !== undefined &&
162
+ <CollectionLoader
163
+ abilities={this.abilitiesToLoad()}
164
+ defaultParams={defaultParams}
165
+ collection={collection}
166
+ component={this}
167
+ modelClass={modelClass}
168
+ noRecordsAvailableContent={noRecordsAvailableContent}
169
+ noRecordsFoundContent={noRecordsFoundContent}
170
+ preloads={preload}
171
+ select={selectCalculator({table: this})}
172
+ selectColumns={selectColumns}
173
+ />
174
+ }
175
+ {showNoRecordsAvailableContent &&
176
+ <div className="live-table--no-records-available-content">
177
+ {noRecordsAvailableContent({models, qParams, overallCount})}
178
+ </div>
179
+ }
180
+ {showNoRecordsFoundContent &&
181
+ <div className="live-table--no-records-found-content">
182
+ {noRecordsFoundContent({models, qParams, overallCount})}
183
+ </div>
184
+ }
185
+ {qParams && query && result && models && !showNoRecordsAvailableContent && !showNoRecordsFoundContent &&
186
+ this.cardOrTable()
187
+ }
188
+ </div>
189
+ )
190
+ }
191
+
192
+ abilitiesToLoad () {
193
+ const abilitiesToLoad = {}
194
+ const {abilities, modelClass} = this.props
195
+ const ownAbilities = []
196
+
197
+ if (this.props.destroyEnabled) {
198
+ ownAbilities.push("destroy")
199
+ }
200
+
201
+ if (this.props.editModelPath) {
202
+ ownAbilities.push("edit")
203
+ }
204
+
205
+ if (this.props.viewModelPath) {
206
+ ownAbilities.push("show")
207
+ }
208
+
209
+ if (ownAbilities.length > 0) {
210
+ const modelClassName = digg(modelClass.modelClassData(), "name")
211
+
212
+ abilitiesToLoad[modelClassName] = ownAbilities
213
+ }
214
+
215
+ if (abilities) {
216
+ for (const modelName in abilities) {
217
+ if (!(modelName in abilitiesToLoad)) {
218
+ abilitiesToLoad[modelName] = []
219
+ }
220
+
221
+ for (const ability of abilities[modelName]) {
222
+ abilitiesToLoad[modelName].push(ability)
223
+ }
224
+ }
225
+ }
226
+
227
+ return abilitiesToLoad
228
+ }
229
+
230
+ cardOrTable () {
231
+ const {
232
+ abilities,
233
+ actionsContent,
234
+ appHistory,
235
+ breakPoint,
236
+ card,
237
+ className,
238
+ collection,
239
+ columns,
240
+ columnsContent,
241
+ controls,
242
+ currentUser,
243
+ defaultDateFormatName,
244
+ defaultDateTimeFormatName,
245
+ defaultParams,
246
+ destroyEnabled,
247
+ destroyMessage,
248
+ editModelPath,
249
+ filterCard,
250
+ filterContent,
251
+ filterSubmitButton,
252
+ filterSubmitLabel,
253
+ groupBy,
254
+ header,
255
+ identifier,
256
+ modelClass,
257
+ noRecordsAvailableContent,
258
+ noRecordsFoundContent,
259
+ onModelsLoaded,
260
+ paginateContent,
261
+ paginationComponent,
262
+ preloads,
263
+ queryName,
264
+ select,
265
+ selectColumns,
266
+ viewModelPath,
267
+ ...restProps
268
+ } = this.props
269
+ const {models, qParams, query, result} = digs(this.shape, "models", "qParams", "query", "result")
270
+
271
+ let controlsContent, headerContent, PaginationComponent
272
+
273
+ if (controls) {
274
+ controlsContent = controls({models, qParams, query, result})
275
+ }
276
+
277
+ if (typeof header == "function") {
278
+ headerContent = header({models, qParams, query, result})
279
+ } else if (header) {
280
+ headerContent = header
281
+ } else {
282
+ headerContent = modelClass.modelName().human({count: 2})
283
+ }
284
+
285
+ if (!paginateContent) {
286
+ if (paginationComponent) {
287
+ PaginationComponent = paginationComponent
288
+ } else {
289
+ PaginationComponent = Paginate
290
+ }
291
+ }
292
+
293
+ const TableComponent = this.responsiveComponent("table")
294
+
295
+ return (
296
+ <>
297
+ {filterContent && filterCard &&
298
+ <Card className="live-table--filter-card mb-4">
299
+ {this.filterForm()}
300
+ </Card>
301
+ }
302
+ {filterContent && !filterCard &&
303
+ this.filterForm()
304
+ }
305
+ {card &&
306
+ <Card className={classNames("mb-4", className)} controls={controlsContent} header={headerContent} table={!this.isSmallScreen()} {...restProps}>
307
+ {this.tableContent()}
308
+ </Card>
309
+ }
310
+ {!card &&
311
+ <TableComponent className={className} {...restProps}>
312
+ {this.tableContent()}
313
+ </TableComponent>
314
+ }
315
+ {result && PaginationComponent &&
316
+ <PaginationComponent result={result} />
317
+ }
318
+ {result && paginateContent &&
319
+ paginateContent({result})
320
+ }
321
+ </>
322
+ )
323
+ }
324
+
325
+ filterForm = () => {
326
+ const {filterFormRef, submitFilter, submitFilterDebounce} = digs(this, "filterFormRef", "submitFilter", "submitFilterDebounce")
327
+ const {filterContent, filterSubmitButton} = digs(this.props, "filterContent", "filterSubmitButton")
328
+ const {filterSubmitLabel} = this.props
329
+ const {qParams} = digs(this.shape, "qParams")
330
+
331
+ return (
332
+ <form className="live-table--filter-form" onSubmit={this.onFilterFormSubmit} ref={filterFormRef}>
333
+ {filterContent({
334
+ onFilterChanged: submitFilter,
335
+ onFilterChangedWithDelay: submitFilterDebounce,
336
+ qParams
337
+ })}
338
+ {filterSubmitButton &&
339
+ <input
340
+ className="btn btn-primary live-table--submit-filter-button"
341
+ type="submit"
342
+ value={filterSubmitLabel || I18n.t("js.api_maker_bootstrap.live_table.filter")}
343
+ />
344
+ }
345
+ </form>
346
+ )
347
+ }
348
+
349
+ tableContent () {
350
+ const {breakPoint} = digs(this.props, "breakPoint")
351
+ const {models, preparedColumns} = digs(this.shape, "models", "preparedColumns")
352
+ const ColumnInHeadComponent = this.columnInHeadComponent()
353
+ const RowComponent = this.rowComponent()
354
+
355
+ let BodyComponent, HeadComponent
356
+
357
+ if (this.isSmallScreen()) {
358
+ BodyComponent = "div"
359
+ HeadComponent = "div"
360
+ } else {
361
+ BodyComponent = "tbody"
362
+ HeadComponent = "thead"
363
+ }
364
+
365
+ return (
366
+ <>
367
+ <HeadComponent>
368
+ <RowComponent className="live-table-header-row">
369
+ {this.headersContentFromColumns()}
370
+ <ColumnInHeadComponent />
371
+ </RowComponent>
372
+ </HeadComponent>
373
+ <BodyComponent>
374
+ {models.map((model) =>
375
+ <ModelRow
376
+ breakPoint={breakPoint}
377
+ columnComponent={this.columnComponent()}
378
+ key={model.id()}
379
+ liveTable={this}
380
+ model={model}
381
+ preparedColumns={preparedColumns}
382
+ rowComponent={this.rowComponent()}
383
+ />
384
+ )}
385
+ </BodyComponent>
386
+ </>
387
+ )
388
+ }
389
+
390
+ className () {
391
+ const classNames = ["component-api-maker-live-table"]
392
+
393
+ if (this.props.className)
394
+ classNames.push(this.props.className)
395
+
396
+ return classNames.join(" ")
397
+ }
398
+
399
+ columnProps(column) {
400
+ const props = {}
401
+
402
+ if (column.textCenter) props["data-text-align"] = "center"
403
+ if (column.textRight) props["data-text-align"] = "right"
404
+
405
+ return props
406
+ }
407
+
408
+ isSmallScreen() {
409
+ if (this.props.breakPoint == "xs" || this.props.breakPoint == "sm") return true
410
+
411
+ return false
412
+ }
413
+
414
+ columnComponent = () => this.responsiveComponent("td")
415
+ columnInHeadComponent = () => this.responsiveComponent("th")
416
+ responsiveComponent = (largeComponent) => this.isSmallScreen() ? "div" : largeComponent
417
+ rowComponent = () => this.responsiveComponent("tr")
418
+
419
+ headersContentFromColumns () {
420
+ const {preparedColumns, query} = digs(this.shape, "preparedColumns", "query")
421
+ const ColumnInHeadComponent = this.columnInHeadComponent()
422
+
423
+ return preparedColumns?.map(({column, tableSettingColumn}) => columnVisible(column, tableSettingColumn) &&
424
+ <ColumnInHeadComponent
425
+ className={classNames(...this.headerClassNameForColumn(column))}
426
+ data-identifier={tableSettingColumn.identifier()}
427
+ key={tableSettingColumn.identifier()}
428
+ {...this.columnProps(column)}
429
+ >
430
+ {tableSettingColumn.hasSortKey() && query &&
431
+ <SortLink attribute={tableSettingColumn.sortKey()} query={query} title={this.headerLabelForColumn(column)} />
432
+ }
433
+ {(!tableSettingColumn.hasSortKey() || !query) &&
434
+ this.headerLabelForColumn(column)
435
+ }
436
+ </ColumnInHeadComponent>
437
+ )
438
+ }
439
+
440
+ headerClassNameForColumn (column) {
441
+ const classNames = ["live-table-header"]
442
+
443
+ if (column.commonProps && column.commonProps.className) classNames.push(column.commonProps.className)
444
+ if (column.headerProps && column.headerProps.className) classNames.push(column.headerProps.className)
445
+
446
+ return classNames
447
+ }
448
+
449
+ headerLabelForColumn (column) {
450
+ const {modelClass} = digs(this.props, "modelClass")
451
+
452
+ if ("label" in column) {
453
+ if (typeof column.label == "function") {
454
+ return column.label()
455
+ } else {
456
+ return column.label
457
+ }
458
+ }
459
+
460
+ let currentModelClass = modelClass
461
+
462
+ // Calculate current model class through path
463
+ if (column.path) {
464
+ for (const pathPart of column.path) {
465
+ const relationships = digg(currentModelClass.modelClassData(), "relationships")
466
+ const relationship = relationships.find((relationshipInArray) => relationshipInArray.name == inflection.underscore(pathPart))
467
+
468
+ currentModelClass = modelClassRequire(digg(relationship, "className"))
469
+ }
470
+ }
471
+
472
+ if (column.attribute) return currentModelClass.humanAttributeName(column.attribute)
473
+
474
+ throw new Error("No 'label' or 'attribute' was given")
475
+ }
476
+
477
+ onFilterFormSubmit = (e) => {
478
+ e.preventDefault()
479
+ this.submitFilter()
480
+ }
481
+
482
+ submitFilter = () => {
483
+ const {filterFormRef} = digs(this, "filterFormRef")
484
+ const filterForm = digg(filterFormRef, "current")
485
+ const {appHistory} = this.props
486
+ const qParams = Params.serializeForm(filterForm)
487
+ const {queryQName} = this.shape
488
+ const changeParamsParams = {}
489
+
490
+ changeParamsParams[queryQName] = qParams
491
+
492
+ Params.changeParams(changeParamsParams, {appHistory})
493
+ }
494
+
495
+ submitFilterDebounce = debounce(digg(this, "submitFilter"))
496
+ }
497
+
498
+ export default withBreakpoint(ApiMakerTable)
@@ -0,0 +1,11 @@
1
+ $xs-from: 0;
2
+ $xs-to: 575;
3
+ $sm-from: 576px;
4
+ $sm-to: 767px;
5
+ $md-from: 768px;
6
+ $md-to: 991px;
7
+ $lg-from: 992px;
8
+ $lg-to: 1199px;
9
+ $xl-from: 1200px;
10
+ $xl-to: 1399px;
11
+ $xxl-from: 1400px;
@@ -0,0 +1,48 @@
1
+ import apiMakerConfig from "@kaspernj/api-maker/src/config.mjs"
2
+ import React from "react"
3
+
4
+ export default (WrappedComponent) => class WithBreakPoint extends React.Component {
5
+ state = {
6
+ breakPoint: this.calculateBreakPoint()
7
+ }
8
+
9
+ calculateBreakPoint() {
10
+ const windowWidth = window.innerWidth
11
+
12
+ for (const breakPointData of apiMakerConfig.getBreakPoints()) {
13
+ const breakPoint = breakPointData[0]
14
+ const width = breakPointData[1]
15
+
16
+ if (windowWidth >= width) return breakPoint
17
+ }
18
+
19
+ throw new Error(`Couldn't not find breakPoint from window width: ${windowWidth}`)
20
+ }
21
+
22
+ constructor(props) {
23
+ super(props)
24
+ this.onCalled = this.onCalled.bind(this)
25
+ }
26
+
27
+ componentDidMount () {
28
+ window.addEventListener("resize", this.onCalled)
29
+ }
30
+
31
+ componentWillUnmount () {
32
+ window.removeEventListener("resize", this.onCalled)
33
+ }
34
+
35
+ render() {
36
+ return (
37
+ <WrappedComponent breakPoint={this.state.breakPoint} {...this.props} />
38
+ )
39
+ }
40
+
41
+ onCalled = () => {
42
+ const breakPoint = this.calculateBreakPoint()
43
+
44
+ if (breakPoint != this.state.breakPoint) {
45
+ this.setState({breakPoint})
46
+ }
47
+ }
48
+ }