@kaspernj/api-maker 1.0.374 → 1.0.376

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaspernj/api-maker",
3
- "version": "1.0.374",
3
+ "version": "1.0.376",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "main": "index.js",
@@ -1,3 +1,4 @@
1
+ import Column from "./column.mjs"
1
2
  import {digg} from "diggerize"
2
3
 
3
4
  export default class ApiMakerBaseModelAttribute {
@@ -5,10 +6,20 @@ export default class ApiMakerBaseModelAttribute {
5
6
  this.attributeData = attributeData
6
7
  }
7
8
 
8
- isColumn() {
9
- return Boolean(digg(this, "attributeData", "column"))
9
+ getColumn() {
10
+ if (!this.column) {
11
+ const columnData = digg(this, "attributeData", "column")
12
+
13
+ if (columnData) {
14
+ this.column = new Column(columnData)
15
+ }
16
+ }
17
+
18
+ return this.column
10
19
  }
11
20
 
21
+ isColumn = () => Boolean(digg(this, "attributeData", "column"))
22
+
12
23
  isSelectedByDefault() {
13
24
  const isSelectedByDefault = digg(this, "attributeData", "selected_by_default")
14
25
 
@@ -17,7 +28,5 @@ export default class ApiMakerBaseModelAttribute {
17
28
  return false
18
29
  }
19
30
 
20
- name() {
21
- return digg(this, "attributeData", "name")
22
- }
31
+ name = () => digg(this, "attributeData", "name")
23
32
  }
@@ -0,0 +1,13 @@
1
+ import {digg} from "diggerize"
2
+
3
+ export default class ApiMakerBaseModelColumn {
4
+ constructor(columnData) {
5
+ if (!columnData) {
6
+ throw new Error("No column data was given")
7
+ }
8
+
9
+ this.columnData = columnData
10
+ }
11
+
12
+ getType = () => digg(this, "columnData", "type")
13
+ }
@@ -1,11 +1,13 @@
1
1
  import classNames from "classnames"
2
2
  import {digg} from "diggerize"
3
+ import * as inflection from "inflection"
3
4
  import MoneyFormatter from "../../money-formatter"
4
5
  import PropTypes from "prop-types"
5
- import React from "react"
6
+ import {memo, useMemo} from "react"
7
+ import {shapeComponent, ShapeComponent} from "set-state-compare/src/shape-component.js"
6
8
  import strftime from "strftime"
7
9
 
8
- export default class ApiMakerBootstrapAttributeRow extends React.PureComponent {
10
+ export default memo(shapeComponent(class ApiMakerBootstrapAttributeRow extends ShapeComponent {
9
11
  static defaultProps = {
10
12
  checkIfAttributeLoaded: false
11
13
  }
@@ -20,6 +22,13 @@ export default class ApiMakerBootstrapAttributeRow extends React.PureComponent {
20
22
  value: PropTypes.node
21
23
  }
22
24
 
25
+ setup() {
26
+ this.attribute = useMemo(
27
+ () => this.props.model?.constructor?.attributes()?.find((attribute) => attribute.name() == inflection.underscore(this.props.attribute)),
28
+ [this.props.attribute, this.props.model]
29
+ )
30
+ }
31
+
23
32
  render () {
24
33
  const {attribute, checkIfAttributeLoaded, children, className, identifier, label, model, value, ...restProps} = this.props
25
34
 
@@ -68,7 +77,11 @@ export default class ApiMakerBootstrapAttributeRow extends React.PureComponent {
68
77
  }
69
78
 
70
79
  valueContent(value) {
71
- if (value instanceof Date) {
80
+ const columnType = this.attribute?.getColumn()?.getType()
81
+
82
+ if (columnType == "date") {
83
+ return I18n.l("date.formats.default", value)
84
+ } else if (value instanceof Date) {
72
85
  return strftime("%Y-%m-%d %H:%M", value)
73
86
  } else if (typeof value === "boolean") {
74
87
  if (value) return I18n.t("js.shared.yes", {defaultValue: "Yes"})
@@ -80,4 +93,4 @@ export default class ApiMakerBootstrapAttributeRow extends React.PureComponent {
80
93
  return value
81
94
  }
82
95
  }
83
- }
96
+ }))
@@ -6,8 +6,10 @@ import SaveSearchModal from "./save-search-modal"
6
6
  import PropTypes from "prop-types"
7
7
  import {memo} from "react"
8
8
  import {shapeComponent, ShapeComponent} from "set-state-compare/src/shape-component.js"
9
+ import {TableSearch} from "../../models.mjs.erb"
9
10
  import useI18n from "i18n-on-steroids/src/use-i18n.mjs"
10
11
  import useQueryParams from "on-location-changed/src/use-query-params"
12
+ import {View} from "react-native"
11
13
 
12
14
  export default memo(shapeComponent(class ApiMakerTableFilters extends ShapeComponent {
13
15
  static propTypes = {
@@ -24,7 +26,7 @@ export default memo(shapeComponent(class ApiMakerTableFilters extends ShapeCompo
24
26
  this.t = t
25
27
  this.useStates({
26
28
  filter: undefined,
27
- showLoadSearchModal: false,
29
+ showLoadSearchModal: undefined,
28
30
  showSaveSearchModal: false
29
31
  })
30
32
  }
@@ -36,7 +38,7 @@ export default memo(shapeComponent(class ApiMakerTableFilters extends ShapeCompo
36
38
  const currentFilters = this.currentFilters()
37
39
 
38
40
  return (
39
- <div className="api-maker--table--filters">
41
+ <View dataSet={{class: "api-maker--table--filters"}}>
40
42
  {filter &&
41
43
  <FilterForm
42
44
  filter={filter}
@@ -50,6 +52,7 @@ export default memo(shapeComponent(class ApiMakerTableFilters extends ShapeCompo
50
52
  <LoadSearchModal
51
53
  currentUser={currentUser}
52
54
  modelClass={modelClass}
55
+ onEditSearchPressed={digg(this, "onEditSearchPressed")}
53
56
  onRequestClose={digg(this, "onRequestCloseLoadSearchModal")}
54
57
  querySearchName={querySName}
55
58
  />
@@ -59,6 +62,7 @@ export default memo(shapeComponent(class ApiMakerTableFilters extends ShapeCompo
59
62
  currentFilters={digg(this, "currentFilters")}
60
63
  currentUser={currentUser}
61
64
  onRequestClose={digg(this, "onRequestCloseSaveSearchModal")}
65
+ search={showSaveSearchModal}
62
66
  />
63
67
  }
64
68
  {currentFilters?.map((filterData, filterIndex) =>
@@ -85,7 +89,7 @@ export default memo(shapeComponent(class ApiMakerTableFilters extends ShapeCompo
85
89
  </>
86
90
  }
87
91
  </div>
88
- </div>
92
+ </View>
89
93
  )
90
94
  }
91
95
 
@@ -109,10 +113,19 @@ export default memo(shapeComponent(class ApiMakerTableFilters extends ShapeCompo
109
113
  }
110
114
 
111
115
  onApplyClicked = () => this.setState({filter: undefined})
116
+
117
+ onEditSearchPressed = ({search}) => {
118
+ this.onRequestCloseLoadSearchModal()
119
+ this.setState({
120
+ showSaveSearchModal: search
121
+ })
122
+ }
123
+
112
124
  onFilterClicked = (args) => this.setState({filter: args})
113
125
 
114
126
  onLoadSearchClicked = (e) => {
115
127
  e.preventDefault()
128
+
116
129
  this.setState({
117
130
  showLoadSearchModal: true
118
131
  })
@@ -136,13 +149,13 @@ export default memo(shapeComponent(class ApiMakerTableFilters extends ShapeCompo
136
149
  }
137
150
 
138
151
  onRequestCloseLoadSearchModal = () => this.setState({showLoadSearchModal: false})
139
- onRequestCloseSaveSearchModal = () => this.setState({showSaveSearchModal: false})
152
+ onRequestCloseSaveSearchModal = () => this.setState({showSaveSearchModal: undefined})
140
153
 
141
154
  onSaveSearchClicked = (e) => {
142
155
  e.preventDefault()
143
156
 
144
157
  if (this.hasAnyFilters()) {
145
- this.setState({showSaveSearchModal: true})
158
+ this.setState({showSaveSearchModal: new TableSearch()})
146
159
  } else {
147
160
  FlashMessage.alert(this.t(".no_filters_has_been_set", {defaultValue: "No filters has been set"}))
148
161
  }
@@ -1,38 +1,127 @@
1
1
  import apiMakerConfig from "@kaspernj/api-maker/src/config.mjs"
2
2
  import classNames from "classnames"
3
3
  import {digg} from "diggerize"
4
+ import {memo, useEffect} from "react"
5
+ import {shapeComponent, ShapeComponent} from "set-state-compare/src/shape-component.js"
4
6
  import {TableSearch} from "../../models.mjs.erb"
5
- import {useCallback, useEffect, useState} from "react"
6
-
7
- const SearchLink = (props) => {
8
- const {onClick, search} = props
9
- const onSearchClicked = useCallback((e) => {
10
- e.preventDefault()
11
- onClick({search})
12
- })
13
-
14
- return (
15
- <a className="load-search-link" href="#" key={search.id()} onClick={onSearchClicked} style={{display: "block"}}>
16
- {search.name()}
17
- </a>
18
- )
19
- }
20
-
21
- const ApiMakerTableFiltersLoadSearchModal = (props) => {
22
- const {className, currentUser, modelClass, onRequestClose, querySearchName, ...restProps} = props
23
- const Modal = apiMakerConfig.getModal()
24
- const [searches, setSearches] = useState(undefined)
25
-
26
- const loadSearches = async () => {
7
+ import {Pressable, Text, View} from "react-native"
8
+ import useI18n from "i18n-on-steroids/src/use-i18n.mjs"
9
+
10
+ const SearchLink = memo(shapeComponent(class SearchLink extends ShapeComponent {
11
+ render() {
12
+ const {search} = this.props
13
+
14
+ return (
15
+ <View dataSet={{class: "search-row", searchId: search.id()}} style={{flexDirection: "row", width: "100%"}}>
16
+ <Pressable dataSet={{class: "load-search-link"}} onPress={this.onSearchClicked} style={{justifyContent: "center"}}>
17
+ <Text>
18
+ {search.name()}
19
+ </Text>
20
+ </Pressable>
21
+ <View style={{flexDirection: "row", marginLeft: "auto"}}>
22
+ <Pressable
23
+ dataSet={{class: "edit-search-button"}}
24
+ onPress={this.onEditPressed}
25
+ style={{
26
+ alignItems: "center",
27
+ justifyContent: "center",
28
+ width: 25,
29
+ height: 25,
30
+ backgroundColor: "#fff",
31
+ border: "1px solid #007bff",
32
+ borderRadius: 5,
33
+ color: "#007bff"
34
+ }}
35
+ >
36
+ <Text>
37
+ &#x270E;
38
+ </Text>
39
+ </Pressable>
40
+ <Pressable
41
+ dataSet={{class: "delete-search-button"}}
42
+ onPress={this.onDeletePressed}
43
+ style={{
44
+ alignItems: "center",
45
+ justifyContent: "center",
46
+ marginLeft: 5,
47
+ width: 25,
48
+ height: 25,
49
+ backgroundColor: "#fff",
50
+ border: "1px solid #dc3545",
51
+ borderRadius: 5,
52
+ color: "#dc3545"
53
+ }}
54
+ >
55
+ <Text>
56
+ &#x2715;
57
+ </Text>
58
+ </Pressable>
59
+ </View>
60
+ </View>
61
+ )
62
+ }
63
+
64
+ onDeletePressed = async () => {
65
+ if (!confirm("Are you sure?")) {
66
+ return
67
+ }
68
+
69
+ const {search} = this.props
70
+
71
+ await search.destroy()
72
+ this.props.onDeleted({search})
73
+ }
74
+
75
+ onEditPressed = () => this.props.onEditPressed({search: this.props.search})
76
+ onSearchClicked = () => this.props.onClick({search: this.props.search})
77
+ }))
78
+
79
+ export default memo(shapeComponent(class ApiMakerTableFiltersLoadSearchModal extends ShapeComponent {
80
+ setup() {
81
+ const {t} = useI18n({namespace: "js.api_maker.table.filters.load_search_modal"})
82
+
83
+ this.useStates({
84
+ editSearch: undefined,
85
+ searches: undefined
86
+ })
87
+ this.setInstance({t})
88
+
89
+ useEffect(() => {
90
+ this.loadSearches()
91
+ }, [])
92
+ }
93
+
94
+ loadSearches = async () => {
95
+ const {currentUser} = this.props
27
96
  const userType = digg(currentUser.modelClassData(), "name")
28
97
  const searches = await TableSearch
29
98
  .ransack({user_id_eq: currentUser.id(), user_type_eq: userType})
30
99
  .toArray()
31
100
 
32
- setSearches(searches)
101
+ this.setState({searches})
33
102
  }
34
103
 
35
- const onSearchClicked = useCallback(({search}) => {
104
+ render() {
105
+ const {t} = this
106
+ const {className, currentUser, modelClass, onEditSearchPressed, onRequestClose, querySearchName, ...restProps} = this.props
107
+ const Modal = apiMakerConfig.getModal()
108
+
109
+ return (
110
+ <Modal className={classNames("api-maker--table--filters--load-search-modal", className)} onRequestClose={onRequestClose} {...restProps}>
111
+ <View>
112
+ <Text>
113
+ {t(".choose_a_search", {defaultValue: "Choose a search"})}
114
+ </Text>
115
+ </View>
116
+ {this.state.searches?.map((search) =>
117
+ <SearchLink key={search.id()} onClick={this.onSearchClicked} onDeleted={this.onSearchDeleted} onEditPressed={onEditSearchPressed} search={search} />
118
+ )}
119
+ </Modal>
120
+ )
121
+ }
122
+
123
+ onSearchClicked = ({search}) => {
124
+ const {onRequestClose, querySearchName} = this.props
36
125
  const queryParams = search.queryParams()
37
126
  const newParams = {}
38
127
 
@@ -41,20 +130,11 @@ const ApiMakerTableFiltersLoadSearchModal = (props) => {
41
130
  Params.changeParams(newParams)
42
131
 
43
132
  onRequestClose()
44
- })
45
-
46
- useEffect(() => { loadSearches() }, [])
47
-
48
- return (
49
- <Modal className={classNames("api-maker--table--filters--load-search-modal", className)} onRequestClose={onRequestClose} {...restProps}>
50
- <div>
51
- {I18n.t("js.api_maker.table.filters.load_search_modal.choose_a_search", {defaultValue: "Choose a search"})}
52
- </div>
53
- {searches?.map((search) =>
54
- <SearchLink key={search.id()} onClick={onSearchClicked} search={search} />
55
- )}
56
- </Modal>
57
- )
58
- }
59
-
60
- export default ApiMakerTableFiltersLoadSearchModal
133
+ }
134
+
135
+ onSearchDeleted = ({search}) => {
136
+ this.setState({
137
+ searches: this.state.searches.filter((existingSearch) => existingSearch.id() != search.id())
138
+ })
139
+ }
140
+ }))
@@ -1,9 +1,8 @@
1
1
  import apiMakerConfig from "@kaspernj/api-maker/src/config.mjs"
2
2
  import Checkbox from "@kaspernj/api-maker/src/bootstrap/checkbox"
3
- import {digg} from "diggerize"
3
+ import {digg, digs} from "diggerize"
4
4
  import Input from "../../bootstrap/input"
5
5
  import {shapeComponent, ShapeComponent} from "set-state-compare/src/shape-component.js"
6
- import {TableSearch} from "../../models.mjs.erb"
7
6
  import {memo} from "react"
8
7
  import useI18n from "i18n-on-steroids/src/use-i18n.mjs"
9
8
 
@@ -15,18 +14,20 @@ export default memo(shapeComponent(class ApiMakerTableFiltersSaveSearchModal ext
15
14
  }
16
15
 
17
16
  render() {
18
- const {currentFilters, currentUser, onRequestClose, ...restProps} = this.props
17
+ const {currentFilters, currentUser, onRequestClose, search, ...restProps} = this.props
19
18
  const Modal = apiMakerConfig.getModal()
20
19
 
21
20
  return (
22
21
  <Modal onRequestClose={onRequestClose} {...restProps}>
23
22
  <form onSubmit={this.onSaveSearchSubmit}>
24
23
  <Input
24
+ defaultValue={search.name()}
25
25
  id="table_search_name"
26
26
  label={this.t(".search_name", {defaultValue: "Search name"})}
27
27
  name="table_search[name]"
28
28
  />
29
29
  <Checkbox
30
+ defaultChecked={search.public()}
30
31
  id="table_search_public"
31
32
  label={this.t(".public", {defaultValue: "Public"})}
32
33
  name="table_search[public]"
@@ -44,15 +45,18 @@ export default memo(shapeComponent(class ApiMakerTableFiltersSaveSearchModal ext
44
45
 
45
46
  const form = digg(e, "target")
46
47
  const formData = new FormData(form)
47
- const tableSearch = new TableSearch()
48
+ const {currentFilters, currentUser, onRequestClose, search} = digs(this.props, "currentFilters", "currentUser", "onRequestClose", "search")
48
49
 
49
- formData.append("table_search[query_params]", JSON.stringify(this.props.currentFilters()))
50
- formData.append("table_search[user_type]", digg(this.props.currentUser.modelClassData(), "className"))
51
- formData.append("table_search[user_id]", this.props.currentUser.id())
50
+ if (search.isNewRecord()) {
51
+ formData.append("table_search[query_params]", JSON.stringify(currentFilters()))
52
+ }
53
+
54
+ formData.append("table_search[user_type]", digg(currentUser.modelClassData(), "className"))
55
+ formData.append("table_search[user_id]", currentUser.id())
52
56
 
53
57
  try {
54
- await tableSearch.saveRaw(formData, {form})
55
- this.props.onRequestClose()
58
+ await search.saveRaw(formData, {form})
59
+ onRequestClose()
56
60
  } catch (error) {
57
61
  FlashMessage.errorResponse(error)
58
62
  }
@@ -105,8 +105,8 @@ export default memo(shapeComponent(class ApiMakerBootStrapLiveTableModelRow exte
105
105
  }
106
106
 
107
107
  columnsContentFromAttributeAndPath (column, model) {
108
- const {attribute} = digs(column, "attribute")
109
- const currentModelClass = this.props.modelClass
108
+ const {attribute: attributeName} = digs(column, "attribute")
109
+ const attributeNameUnderscore = inflection.underscore(attributeName)
110
110
  const path = column.path || []
111
111
  let value
112
112
  let currentModel = model
@@ -118,8 +118,18 @@ export default memo(shapeComponent(class ApiMakerBootStrapLiveTableModelRow exte
118
118
  }
119
119
  }
120
120
 
121
- if (!(attribute in currentModel)) throw new Error(`${currentModelClass.modelName().name} doesn't respond to ${attribute}`)
122
- if (currentModel.isAttributeLoaded(attribute)) value = currentModel[attribute]()
121
+ if (!(attributeName in currentModel)) {
122
+ throw new Error(`${currentModel.constructor.modelName().human()} doesn't respond to ${attributeName}`)
123
+ }
124
+
125
+ if (currentModel.isAttributeLoaded(attributeName)) value = currentModel[attributeName]()
126
+
127
+ const attribute = currentModel.constructor.attributes().find((attribute) => attribute.name() == attributeNameUnderscore)
128
+ const modelColumn = attribute?.getColumn()
129
+
130
+ if (modelColumn?.getType() == "date") {
131
+ return this.presentDateTime({apiMakerType: "date", value})
132
+ }
123
133
 
124
134
  return this.presentColumnValue(value)
125
135
  }
@@ -155,9 +165,9 @@ export default memo(shapeComponent(class ApiMakerBootStrapLiveTableModelRow exte
155
165
  }
156
166
  }
157
167
 
158
- presentColumnValue (value) {
168
+ presentColumnValue(value) {
159
169
  if (value instanceof Date) {
160
- return this.presentDateTime(value)
170
+ return this.presentDateTime({value})
161
171
  } else if (MoneyFormatter.isMoney(value)) {
162
172
  return MoneyFormatter.format(value)
163
173
  } else if (typeof value == "boolean") {
@@ -174,15 +184,13 @@ export default memo(shapeComponent(class ApiMakerBootStrapLiveTableModelRow exte
174
184
  return value
175
185
  }
176
186
 
177
- presentDateTime(value) {
178
- const apiMakerType = value.apiMakerType || "time"
179
-
180
- if (apiMakerType == "time") {
187
+ presentDateTime({apiMakerType, value}) {
188
+ if (!apiMakerType || apiMakerType == "time") {
181
189
  const dateTimeFormatName = this.props.liveTable.props.defaultDateTimeFormatName || "time.formats.default"
182
190
 
183
191
  return I18n.l(dateTimeFormatName, value)
184
192
  } else if (apiMakerType == "date") {
185
- const dateFormatName = this.props.liveTable.props.defaultDateTimeFormatName || "date.formats.default"
193
+ const dateFormatName = this.props.liveTable.props.defaultDateFormatName || "date.formats.default"
186
194
 
187
195
  return I18n.l(dateFormatName, value)
188
196
  } else {