@afeefa/vue-app 0.0.110 → 0.0.112

Sign up to get free protection for your applications and to get access to all the features.
@@ -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>