@afeefa/vue-app 0.0.110 → 0.0.112

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.
@@ -1 +1 @@
1
- 0.0.110
1
+ 0.0.112
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
1
  # @afeefa/vue-app
2
2
 
3
3
  Force push :-);
4
+
5
+ ## Documentation
6
+
7
+ * [Components / AModal](docs/components/amodal.md)
@@ -0,0 +1,19 @@
1
+ # AModal
2
+
3
+ It's a Modal capsule for vuetifys Modals. Whats the magic about it?
4
+
5
+ The Modal is by default relatively positioned near its activator.
6
+
7
+ ## Properties
8
+
9
+ * show
10
+ * icon
11
+ * title
12
+ * beforeClose
13
+ * anchorPosition
14
+ * screenCentered
15
+
16
+
17
+ ### screenCentered
18
+
19
+ if set to true the Modal is positioned in the middle of the Viewport, like in vuetify's default beavior.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afeefa/vue-app",
3
- "version": "0.0.110",
3
+ "version": "0.0.112",
4
4
  "description": "",
5
5
  "author": "Afeefa Kollektiv <kollektiv@afeefa.de>",
6
6
  "license": "MIT",
@@ -10,6 +10,7 @@ export class ApiAction extends ApiResourcesApiAction {
10
10
  _dispatchGlobalSaveEvents = false
11
11
  _minDuration = 100
12
12
  _startTime = 0
13
+ _showError = true
13
14
 
14
15
  id (id) {
15
16
  this.param('id', id)
@@ -21,6 +22,11 @@ export class ApiAction extends ApiResourcesApiAction {
21
22
  return this
22
23
  }
23
24
 
25
+ hideError () {
26
+ this._showError = false
27
+ return this
28
+ }
29
+
24
30
  dispatchGlobalLoadingEvents (dispatch = true) {
25
31
  this._dispatchGlobalLoadingEvents = dispatch
26
32
  return this
@@ -27,10 +27,12 @@ export class DeleteAction extends ApiAction {
27
27
  }
28
28
 
29
29
  processError (result) {
30
- eventBus.dispatch(new AlertEvent(AlertEvent.ERROR, {
31
- headline: 'Die Daten konnten nicht gelöscht werden.',
32
- message: result.message,
33
- detail: result.detail
34
- }))
30
+ if (this._showError) {
31
+ eventBus.dispatch(new AlertEvent(AlertEvent.ERROR, {
32
+ headline: 'Die Daten konnten nicht gelöscht werden.',
33
+ message: result.message,
34
+ detail: result.detail
35
+ }))
36
+ }
35
37
  }
36
38
  }
@@ -9,10 +9,12 @@ export class GetAction extends ApiAction {
9
9
  }
10
10
 
11
11
  processError (result) {
12
- eventBus.dispatch(new AlertEvent(AlertEvent.ERROR, {
13
- headline: 'Die Daten konntent nicht geladen werden.',
14
- message: result.message,
15
- detail: result.detail
16
- }))
12
+ if (this._showError) {
13
+ eventBus.dispatch(new AlertEvent(AlertEvent.ERROR, {
14
+ headline: 'Die Daten konntent nicht geladen werden.',
15
+ message: result.message,
16
+ detail: result.detail
17
+ }))
18
+ }
17
19
  }
18
20
  }
@@ -17,10 +17,12 @@ export class SaveAction extends ApiAction {
17
17
  }
18
18
 
19
19
  processError (result) {
20
- eventBus.dispatch(new AlertEvent(AlertEvent.ERROR, {
21
- headline: 'Die Daten konnten nicht gespeichert werden.',
22
- message: result.message,
23
- detail: result.detail
24
- }))
20
+ if (this._showError) {
21
+ eventBus.dispatch(new AlertEvent(AlertEvent.ERROR, {
22
+ headline: 'Die Daten konnten nicht gespeichert werden.',
23
+ message: result.message,
24
+ detail: result.detail
25
+ }))
26
+ }
25
27
  }
26
28
  }
@@ -11,7 +11,7 @@
11
11
  no-filter
12
12
 
13
13
  :rules="validationRules"
14
- v-bind="$attrs"
14
+ v-bind="{...$attrs, dense, outlined}"
15
15
  v-on="$listeners"
16
16
  />
17
17
  </template>
@@ -23,7 +23,7 @@ import { Model } from '@afeefa/api-resources-client'
23
23
  import { debounce } from '@a-vue/utils/debounce'
24
24
 
25
25
  @Component({
26
- props: ['items', 'validator', 'defaultValue', 'selectedItemText', 'debounce']
26
+ props: ['items', 'validator', 'defaultValue', 'selectedItemText', 'debounce', {dense: true, outlined: true}]
27
27
  })
28
28
  export default class AAutocomplete extends Vue {
29
29
  isLoading = false
@@ -12,7 +12,7 @@
12
12
  :value="formattedDate"
13
13
  :label="label"
14
14
  :style="cwm_widthStyle"
15
- v-bind="{...$attrs, ...attrs}"
15
+ v-bind="{...$attrs, ...attrs, dense, outlined}"
16
16
  :rules="validationRules"
17
17
  :error-messages="errorMessages"
18
18
  :readonly="type === 'month'"
@@ -41,7 +41,7 @@ import { formatDate } from '@a-vue/utils/format-date'
41
41
  import { ComponentWidthMixin } from './mixins/ComponentWidthMixin'
42
42
 
43
43
  @Component({
44
- props: ['value', 'validator', 'type']
44
+ props: ['value', 'validator', 'type', {dense: true, outlined: true}]
45
45
  })
46
46
  export default class ADatePicker extends Mixins(ComponentWidthMixin) {
47
47
  value_ = null
@@ -153,8 +153,13 @@ export default class ADatePicker extends Mixins(ComponentWidthMixin) {
153
153
  <style lang="scss" scoped>
154
154
  :deep(.v-select__slot) {
155
155
  cursor: pointer;
156
+
156
157
  input {
157
158
  cursor: pointer;
158
159
  }
159
160
  }
161
+
162
+ .v-text-field :deep(.v-input__icon--clear) { // always show clear icon, https://github.com/vuetifyjs/vuetify/pull/15876
163
+ opacity: 1;
164
+ }
160
165
  </style>
@@ -1,5 +1,8 @@
1
1
  <template>
2
- <div :class="['a-grid', colsClass, gapClass, evenClass, breakMobileClass]">
2
+ <div
3
+ :style="{display: displayStyle}"
4
+ :class="['a-grid', colsClass, gapClass, evenClass, breakMobileClass]"
5
+ >
3
6
  <slot />
4
7
  </div>
5
8
  </template>
@@ -10,7 +13,7 @@ import { Component, Mixins } from '@a-vue'
10
13
  import { ComponentWidthMixin } from './mixins/ComponentWidthMixin'
11
14
 
12
15
  @Component({
13
- props: ['gap', 'hGap', 'vGap', 'cols', {even: false}, 'breakMobile']
16
+ props: ['gap', 'hGap', 'vGap', 'cols', {inline: false, even: false}, 'breakMobile']
14
17
  })
15
18
  export default class AGrid extends Mixins(ComponentWidthMixin) {
16
19
  get breakMobileClass () {
@@ -19,6 +22,10 @@ export default class AGrid extends Mixins(ComponentWidthMixin) {
19
22
  }
20
23
  }
21
24
 
25
+ get displayStyle () {
26
+ return this.inline ? 'inline-grid' : 'grid'
27
+ }
28
+
22
29
  get colsClass () {
23
30
  const cols = this.cols || 2
24
31
  return 'cols-' + cols
@@ -53,11 +60,10 @@ export default class AGrid extends Mixins(ComponentWidthMixin) {
53
60
 
54
61
  <style scoped lang="scss">
55
62
  .a-grid {
56
- display: grid;
57
-
58
63
  @for $i from 1 through 8 {
59
64
  &.cols-#{$i} {
60
65
  grid-template-columns: repeat(#{$i}, auto);
66
+
61
67
  &.even {
62
68
  grid-template-columns: repeat(#{$i}, 1fr);
63
69
  }
@@ -67,6 +73,7 @@ export default class AGrid extends Mixins(ComponentWidthMixin) {
67
73
  &.breakMobile {
68
74
  @media (max-width: 900px), (orientation : portrait) {
69
75
  grid-template-columns: 1fr;
76
+
70
77
  &.even {
71
78
  grid-template-columns: 1fr;
72
79
  }
@@ -5,7 +5,7 @@
5
5
  :items="items_"
6
6
  :valueComparator="compareValues"
7
7
  :style="cwm_widthStyle"
8
- v-bind="$attrs"
8
+ v-bind="{...$attrs, dense, outlined}"
9
9
  v-on="$listeners"
10
10
  />
11
11
  </template>
@@ -17,7 +17,7 @@ import { Model } from '@afeefa/api-resources-client'
17
17
  import { ComponentWidthMixin } from './mixins/ComponentWidthMixin'
18
18
 
19
19
  @Component({
20
- props: ['validator', 'defaultValue', 'items']
20
+ props: ['validator', 'defaultValue', 'items', {dense: true, outlined: true}]
21
21
  })
22
22
  export default class ASelect extends Mixins(ComponentWidthMixin) {
23
23
  items_ = []
@@ -96,3 +96,10 @@ export default class ASelect extends Mixins(ComponentWidthMixin) {
96
96
  }
97
97
  }
98
98
  </script>
99
+
100
+
101
+ <style lang="scss" scoped>
102
+ .v-text-field :deep(.v-input__icon--clear) { // always show clear icon, https://github.com/vuetifyjs/vuetify/pull/15876
103
+ opacity: 1;
104
+ }
105
+ </style>
@@ -3,7 +3,7 @@
3
3
  ref="input"
4
4
  :rules="validationRules"
5
5
  :counter="counter"
6
- v-bind="$attrs"
6
+ v-bind="{...$attrs, dense, outlined}"
7
7
  v-on="$listeners"
8
8
  />
9
9
  </template>
@@ -13,7 +13,7 @@
13
13
  import { Component, Vue } from '@a-vue'
14
14
 
15
15
  @Component({
16
- props: ['validator']
16
+ props: ['validator', {dense: true, outlined: true}]
17
17
  })
18
18
  export default class ATextArea extends Vue {
19
19
  mounted () {
@@ -1,45 +1,150 @@
1
1
  <template>
2
2
  <v-text-field
3
3
  ref="input"
4
- :type="type"
5
- :autocomplete="autocomplete"
6
- :rules="validationRules"
4
+ v-model="internalValue"
7
5
  :counter="counter"
8
6
  :style="cwm_widthStyle"
9
- v-bind="$attrs"
10
- v-on="$listeners"
11
- @click:append="showPassword = !showPassword"
7
+ :error-messages="errorMessages"
8
+ v-bind="attrs"
9
+ @input="inputChanged"
10
+ @keyup.esc="clear"
11
+ @click:clear="clear"
12
+ v-on="listeners"
12
13
  />
13
14
  </template>
14
15
 
15
16
 
16
17
  <script>
17
- import { Component, Watch, Mixins } from '@a-vue'
18
+ import { Component, Watch, Mixins, Inject } from '@a-vue'
18
19
  import { debounce } from '@a-vue/utils/debounce'
19
20
  import { ComponentWidthMixin } from './mixins/ComponentWidthMixin'
20
21
 
21
22
  @Component({
22
- props: ['debounce', 'validator', {focus: false, password: false, number: false}]
23
+ props: ['value', 'debounce', 'validator', 'rules', {dense: true, outlined: true, clearable: false, focus: false, number: false}]
23
24
  })
24
25
  export default class ATextField extends Mixins(ComponentWidthMixin) {
25
26
  $hasOptions = ['counter']
26
27
 
27
- showPassword = false
28
+ @Inject({ from: 'form', default: null }) form
29
+
30
+ internalValue = null
31
+ errorMessages = []
32
+ debounceInputFunction = null
28
33
 
29
34
  created () {
30
- if (this.debounce) {
31
- this.$listeners.input = debounce(value => {
32
- this.$emit('input', value)
33
- }, this.debounce, value => value)
34
- }
35
+ this.form && this.form.register(this)
36
+
37
+ this.setInternalValue(this.value)
35
38
  }
36
39
 
37
40
  mounted () {
38
41
  this.setFocus()
39
42
 
40
- if (this.validator) {
41
- this.$refs.input.validate(true)
43
+ this.$emit('input:internal', this.internalValue)
44
+ this.validate()
45
+ }
46
+
47
+ setInternalValue (value) {
48
+ if (typeof value === 'number') {
49
+ value = value.toString()
50
+ }
51
+ this.internalValue = value || ''
52
+ }
53
+
54
+ @Watch('value')
55
+ valueChanged () {
56
+ this.setInternalValue(this.value)
57
+ }
58
+
59
+ get listeners () {
60
+ // remove input from nested listening
61
+ // let clients listend to only THIS component
62
+ const listeners = {...this.$listeners}
63
+ delete listeners.input
64
+ return listeners
65
+ }
66
+
67
+ get attrs () {
68
+ // remove 'rules' from being passed to v-text-field
69
+ const attrs = {...this.$attrs}
70
+ delete attrs.rules
71
+
72
+ attrs.dense = this.dense
73
+ attrs.outlined = this.outlined
74
+ attrs.clearable = this.clearable
75
+ return attrs
76
+ }
77
+
78
+ clear () {
79
+ if (this.clearable) {
80
+ this.setInternalValue('')
81
+ this.$emit('input', this.emptyValue)
82
+ this.validate()
83
+ }
84
+ }
85
+
86
+ inputChanged () {
87
+ this.$emit('input:internal', this.internalValue)
88
+
89
+ const valid = this.validate()
90
+
91
+ if (!valid) {
92
+ return
93
+ }
94
+
95
+ const value = this.stringToNumber(this.internalValue)
96
+
97
+ // NaN means: wrong numerical value AND no validator present
98
+ // otherwise validator would return validate() -> false
99
+ if (Number.isNaN(value)) {
100
+ return
101
+ }
102
+
103
+ if (this.debounce) {
104
+ if (!this.debounceInputFunction) {
105
+ this.debounceInputFunction = debounce(value => {
106
+ this.$emit('input', value)
107
+ }, this.debounce, value => value) // fire immediately if !value (click clearable-x)
108
+ }
109
+ this.debounceInputFunction(value)
110
+ } else {
111
+ this.$emit('input', value)
112
+ }
113
+ }
114
+
115
+ stringToNumber (value) {
116
+ if (this.treatAsNumericValue) {
117
+ if (!value) {
118
+ value = null
119
+ } else {
120
+ value = this.internalValue.match(/^\d*(.\d+)?$/) ? parseFloat(this.internalValue) : NaN
121
+ }
42
122
  }
123
+ return value
124
+ }
125
+
126
+ get type () {
127
+ return this.$attrs.type || 'text'
128
+ }
129
+
130
+ get treatAsNumericValue () {
131
+ return this.type === 'number' || this.number
132
+ }
133
+
134
+ validate () {
135
+ const rules = this.validationRules
136
+ let errorMessage = null
137
+ for (const rule of rules) {
138
+ const value = this.stringToNumber(this.internalValue)
139
+ const result = rule(value)
140
+ if (result !== true) {
141
+ errorMessage = result
142
+ break
143
+ }
144
+ }
145
+
146
+ this.errorMessages = [errorMessage].filter(e => e)
147
+ return !this.errorMessages.length
43
148
  }
44
149
 
45
150
  @Watch('focus')
@@ -58,49 +163,42 @@ export default class ATextField extends Mixins(ComponentWidthMixin) {
58
163
  }
59
164
  }
60
165
 
61
- get type () {
62
- if (this.password && !this.showPassword) {
63
- return 'password'
64
- }
65
- return 'text'
66
- }
67
-
68
- get appendIcon () {
69
- if (this.password) {
70
- return this.showPassword ? '$eyeIcon' : '$eyeOffIcon'
71
- }
72
- return null
73
- }
74
-
75
- get autocomplete () {
76
- if (this.password) {
77
- return 'new-password'
78
- }
79
- return null
80
- }
81
-
82
166
  get validationRules () {
167
+ if (this.$attrs.rules) {
168
+ return this.$attrs.rules
169
+ }
83
170
  const label = this.$attrs.label
84
171
  return (this.validator && this.validator.getRules(label)) || []
85
172
  }
86
173
 
87
174
  get counter () {
88
175
  if (!this.$has.counter) {
89
- return false
176
+ return null
90
177
  }
91
178
 
92
179
  if (!this.validator) {
93
- return false
180
+ return null
94
181
  }
95
182
 
96
- return (!this.number && this.validator.getParams().max) || false
183
+ return this.validator.getMaxValueLength()
184
+ }
185
+
186
+ get emptyValue () {
187
+ if (this.validator) {
188
+ return this.validator.getEmptyValue()
189
+ }
190
+ return null
97
191
  }
98
192
  }
99
193
  </script>
100
194
 
101
195
 
102
196
  <style lang="scss" scoped>
103
- .v-input:not(.v-input--is-focused) :deep(.v-counter) {
197
+ .v-input:not(.v-input--is-focused) :deep(.v-counter) { // hide counter when not focused
104
198
  display: none;
105
199
  }
200
+
201
+ .v-text-field :deep(.v-input__icon--clear) { // always show clear icon, https://github.com/vuetifyjs/vuetify/pull/15876
202
+ opacity: 1;
203
+ }
106
204
  </style>
@@ -44,7 +44,7 @@
44
44
  small
45
45
  angular
46
46
  :has="{reset: !!modelToEdit.id}"
47
- @save="$emit('save', modelToEdit, ignoreChangesOnClose)"
47
+ @save="$emit('save', modelToEdit, ignoreChangesOnClose, close)"
48
48
  @reset="$refs.form.reset()"
49
49
  />
50
50
  </a-row>
@@ -68,6 +68,10 @@ export default class EditModal extends Vue {
68
68
  ignoreChangesOnClose_ = false
69
69
 
70
70
  created () {
71
+ if (!this.model && !this.createModelToEdit) {
72
+ console.warn('You need to pass either a model or a model factory to <edit-modal>.')
73
+ }
74
+
71
75
  if (this.show) { // open on create with v-show
72
76
  this.open()
73
77
  }
@@ -1,12 +1,10 @@
1
1
  <template>
2
2
  <a-text-field
3
- :value="internalValue"
3
+ v-model="model[name]"
4
4
  :label="label || name"
5
5
  :validator="validator"
6
6
  v-bind="$attrs"
7
7
  v-on="$listeners"
8
- @input="textFieldValueChanged"
9
- @blur="onBlur"
10
8
  />
11
9
  </template>
12
10
 
@@ -14,71 +12,7 @@
14
12
  import { Component, Mixins } from '@a-vue'
15
13
  import { FormFieldMixin } from '../FormFieldMixin'
16
14
 
17
- @Component({
18
- props: [{emptyNull: false}]
19
- })
15
+ @Component
20
16
  export default class FormFieldText extends Mixins(FormFieldMixin) {
21
- internalValue = ''
22
-
23
- created () {
24
- this.setInternalValue(this.model[this.name])
25
- this.$watch(() => this.model[this.name], value => {
26
- this.setInternalValue(value)
27
- })
28
- }
29
-
30
- onBlur () {
31
- this.setInternalValue(this.model[this.name], true)
32
- }
33
-
34
- textFieldValueChanged (value) {
35
- this.internalValue = value
36
-
37
- // cast to number
38
- if (this.isNumber) {
39
- value = Number(value)
40
- if (isNaN(value)) {
41
- return // do not set anything to the model
42
- }
43
- }
44
-
45
- // set model value to null if empty
46
- if (this.emptyNull) {
47
- if (this.isNumber) {
48
- if (value === 0) {
49
- value = null
50
- }
51
- } else {
52
- if (!value) {
53
- value = null
54
- }
55
- }
56
- }
57
-
58
- this.model[this.name] = value
59
- this.$emit('input', value)
60
- }
61
-
62
- setInternalValue (value, reset = false) {
63
- if (this.isNumber) {
64
- // reset text field if value is null but keep leading 0 (allows for copy and paste)
65
- if (value === null) {
66
- if (!reset && this.internalValue === '0') {
67
- value = '0'
68
- } else {
69
- value = ''
70
- }
71
- }
72
- } else { // null string should be ''
73
- if (!value) {
74
- value = ''
75
- }
76
- }
77
- this.internalValue = value
78
- }
79
-
80
- get isNumber () {
81
- return this.$attrs.number === ''
82
- }
83
17
  }
84
18
  </script>
@@ -15,7 +15,6 @@ import NestedEditForm from './form/NestedEditForm'
15
15
  import ListFilterPage from './list/filters/ListFilterPage'
16
16
  import ListFilterSearch from './list/filters/ListFilterSearch'
17
17
  import ListFilterSelect from './list/filters/ListFilterSelect'
18
- import ListFilterRow from './list/ListFilterRow'
19
18
 
20
19
  Vue.component('EditForm', EditForm)
21
20
  Vue.component('NestedEditForm', NestedEditForm)
@@ -31,5 +30,4 @@ Vue.component('FormFieldSelect', FormFieldSelect)
31
30
  Vue.component('FormFieldSelect2', FormFieldSelect2)
32
31
  Vue.component('ListFilterPage', ListFilterPage)
33
32
  Vue.component('ListFilterSearch', ListFilterSearch)
34
- Vue.component('ListFilterRow', ListFilterRow)
35
33
  Vue.component('ListFilterSelect', ListFilterSelect)
@@ -1,7 +1,5 @@
1
1
  <template>
2
- <a-row
3
- gap="8"
4
- >
2
+ <a-row gap="8">
5
3
  <a-pagination
6
4
  v-if="count && numPages > 1"
7
5
  v-model="filter.value"
@@ -20,6 +18,7 @@
20
18
  :items="pageSizeFilter.options"
21
19
  :defaultValue="pageSizeFilter.defaultValue"
22
20
  :clearable="!pageSizeFilter.hasDefaultValueSet()"
21
+ hide-details
23
22
  />
24
23
  </a-row>
25
24
  </template>
@@ -54,7 +53,7 @@ export default class ListFilterPage extends Mixins(ListFilterMixin) {
54
53
 
55
54
  <style lang="scss" scoped>
56
55
  .v-select {
57
- max-width: 100px;
56
+ max-width: 150px;
58
57
  }
59
58
 
60
59
  .pageNumber {
@@ -6,6 +6,7 @@
6
6
  :debounce="500"
7
7
  v-bind="$attrs"
8
8
  clearable
9
+ hide-details
9
10
  @keyup.esc="clearValue"
10
11
  />
11
12
  </template>
@@ -7,6 +7,7 @@
7
7
  itemValue="itemValue"
8
8
  :clearable="filter.value !== filter.defaultValue"
9
9
  :defaultValue="filter.defaultValue"
10
+ hide-details
10
11
  v-bind="$attrs"
11
12
  />
12
13
  </template>
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import './styles/vue-app.scss'
2
2
 
3
- export { Mixins, Vue, Watch } from 'vue-property-decorator'
3
+ export { Mixins, Vue, Watch, Inject, Provide } from 'vue-property-decorator'
4
4
  export { Component } from '@a-vue/components/vue/Component'
@@ -76,7 +76,7 @@
76
76
  </template>
77
77
 
78
78
  <template #filters>
79
- <list-filter-row>
79
+ <a-row gap="4">
80
80
  <list-filter-search
81
81
  :focus="true"
82
82
  maxWidth="100%"
@@ -87,7 +87,7 @@
87
87
  :has="{page_size: false, page_number: true}"
88
88
  :totalVisible="0"
89
89
  />
90
- </list-filter-row>
90
+ </a-row>
91
91
  </template>
92
92
 
93
93
  <template #row="{ model, on }">
@@ -1,26 +0,0 @@
1
- <template>
2
- <a-row
3
- :gap="gap || 4"
4
- class="mb-0"
5
- >
6
- <slot />
7
- </a-row>
8
- </template>
9
-
10
-
11
- <script>
12
- import { Component, Vue } from '@a-vue'
13
-
14
- @Component({
15
- props: ['gap']
16
- })
17
- export default class ListFilterRow extends Vue {
18
- }
19
- </script>
20
-
21
-
22
- <style scoped lang="scss">
23
- :deep(.v-text-field__details) {
24
- display: none;
25
- }
26
- </style>