@afeefa/vue-app 0.0.54 → 0.0.57

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 (35) hide show
  1. package/.afeefa/package/release/version.txt +1 -1
  2. package/package.json +1 -1
  3. package/src/components/AContextMenu.vue +1 -1
  4. package/src/components/AContextMenuItem.vue +1 -1
  5. package/src/components/ADialog.vue +2 -0
  6. package/src/components/ARichTextArea.vue +257 -0
  7. package/src/components/ASearchSelect.vue +12 -12
  8. package/src/components/form/EditForm.vue +1 -5
  9. package/src/components/form/EditModal.vue +22 -17
  10. package/src/components/form/FormFieldMixin.js +2 -3
  11. package/src/components/form/fields/FormFieldRichTextArea.vue +14 -0
  12. package/src/components/index.js +2 -0
  13. package/src/components/list/ListViewMixin.js +4 -0
  14. package/src/components/list/filters/ListFilterSelect.vue +4 -2
  15. package/src/components/search-select/SearchSelectList.vue +5 -1
  16. package/src/index.js +2 -0
  17. package/src/plugins/api-resources/ApiResourcesPlugin.js +12 -0
  18. package/src/styles/forms.scss +8 -0
  19. package/src/styles/vue-app.scss +1 -0
  20. package/src-admin/bootstrap.js +2 -5
  21. package/src-admin/components/controls/SearchSelectFormField.vue +223 -0
  22. package/src-admin/components/detail/DetailProperty.vue +1 -3
  23. package/src-admin/components/list/ListColumnHeader.vue +4 -3
  24. package/src-admin/components/list/ListView.vue +15 -5
  25. package/src-admin/components/pages/CreatePage.vue +1 -2
  26. package/src-admin/components/pages/DetailPage.vue +3 -3
  27. package/src-admin/components/pages/EditPage.vue +4 -5
  28. package/src-admin/components/pages/ListPage.vue +2 -3
  29. package/src-admin/components/routes/DetailRoute.vue +3 -3
  30. package/src-admin/components/routes/EditRoute.vue +3 -3
  31. package/src-admin/components/routes/ListRoute.vue +3 -3
  32. package/src-admin/config/vuetify.js +3 -1
  33. package/src-admin/models/Model.js +13 -0
  34. package/src-admin/models/ModelAdminConfig.js +20 -0
  35. package/src-components/AMdiIcon.vue +18 -0
@@ -1 +1 @@
1
- 0.0.54
1
+ 0.0.57
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afeefa/vue-app",
3
- "version": "0.0.54",
3
+ "version": "0.0.57",
4
4
  "description": "",
5
5
  "author": "Afeefa Kollektiv <kollektiv@afeefa.de>",
6
6
  "license": "MIT",
@@ -140,7 +140,7 @@ export default class AContextMenu extends Mixins(UsesPositionServiceMixin) {
140
140
  .popUpContent {
141
141
  min-height: 2.2rem;
142
142
  position: absolute;
143
- z-index: 200;
143
+ z-index: 400;
144
144
  display: block;
145
145
  background-color: white;
146
146
  padding: 0.5rem;
@@ -29,7 +29,7 @@ export default class AContextMenuItem extends Vue {
29
29
  this.contextMenu.close()
30
30
  if (this.to) {
31
31
  this.$router.push(this.to)
32
- .catch(() => null) // prevent duplicated navigation
32
+ .catch(() => null) // prevent duplicated navigation warning
33
33
  } else {
34
34
  this.$emit('click')
35
35
  }
@@ -132,6 +132,8 @@ export default class ADialog extends Mixins(UsesPositionServiceMixin) {
132
132
  if (!Array.isArray(anchor)) {
133
133
  if (typeof anchor === 'string') {
134
134
  anchor = [document.documentElement, anchor]
135
+ } else if (typeof anchor === 'object') { // dom element or vue ref
136
+ anchor = [anchor]
135
137
  } else {
136
138
  anchor = [document.documentElement]
137
139
  }
@@ -0,0 +1,257 @@
1
+ <template>
2
+ <div :class="['a-rich-text-editor a-text-input', {'a-text-input-focused': focus}]">
3
+ <div
4
+ v-if="editor"
5
+ class="menu-bar"
6
+ >
7
+ <v-btn
8
+ small
9
+ :class="['menu-button', {'is-active': focus && editor.isActive('bold')}]"
10
+ @click="editor.chain().focus().toggleBold().run()"
11
+ >
12
+ <v-icon>{{ boldIcon }}</v-icon>
13
+ </v-btn>
14
+
15
+ <v-btn
16
+ small
17
+ :class="['menu-button', {'is-active': focus && editor.isActive('italic')}]"
18
+ @click="editor.chain().focus().toggleItalic().run()"
19
+ >
20
+ <v-icon>{{ italicIcon }}</v-icon>
21
+ </v-btn>
22
+
23
+ <v-btn
24
+ small
25
+ :class="['menu-button', 'strike', {'is-active': focus && editor.isActive('strike')}]"
26
+ @click="editor.chain().focus().toggleStrike().run()"
27
+ >
28
+ <v-icon>{{ strikeIcon }}</v-icon>
29
+ </v-btn>
30
+
31
+ <v-btn
32
+ small
33
+ :class="['menu-button', {'is-active': focus && editor.isActive('heading', {level: 1})}]"
34
+ @click="editor.chain().focus().toggleHeading({level: 1}).run()"
35
+ >
36
+ <v-icon>{{ h1Icon }}</v-icon>
37
+ </v-btn>
38
+
39
+ <v-btn
40
+ small
41
+ :class="['menu-button', {'is-active': focus && editor.isActive('heading', {level: 2})}]"
42
+ @click="editor.chain().focus().toggleHeading({level: 2}).run()"
43
+ >
44
+ <v-icon>{{ h2Icon }}</v-icon>
45
+ </v-btn>
46
+
47
+ <v-btn
48
+ small
49
+ :class="['menu-button', {'is-active': focus && editor.isActive('bulletList')}]"
50
+ @click="editor.chain().focus().toggleBulletList().run()"
51
+ >
52
+ <v-icon>{{ ulIcon }}</v-icon>
53
+ </v-btn>
54
+
55
+ <v-btn
56
+ small
57
+ :class="['menu-button', {'is-active': focus && editor.isActive('orderedList')}]"
58
+ @click="editor.chain().focus().toggleOrderedList().run()"
59
+ >
60
+ <v-icon>{{ olIcon }}</v-icon>
61
+ </v-btn>
62
+
63
+ <v-btn
64
+ small
65
+ :class="['menu-button', {'is-active': focus && editor.isActive('blockquote')}]"
66
+ @click="editor.chain().focus().toggleBlockquote().run()"
67
+ >
68
+ <v-icon>{{ commentIcon }}</v-icon>
69
+ </v-btn>
70
+
71
+ <v-btn
72
+ small
73
+ class="menu-button"
74
+ @click="editor.chain().focus().undo().run()"
75
+ >
76
+ <v-icon>{{ undoIcon }}</v-icon>
77
+ </v-btn>
78
+
79
+ <v-btn
80
+ small
81
+ class="menu-button"
82
+ @click="editor.chain().focus().redo().run()"
83
+ >
84
+ <v-icon>{{ redoIcon }}</v-icon>
85
+ </v-btn>
86
+ </div>
87
+
88
+ <editor-content
89
+ :editor="editor"
90
+ :class="['a-rich-text-editor', {focus}]"
91
+ />
92
+ </div>
93
+ </template>
94
+
95
+
96
+ <script>
97
+ import { Component, Vue, Watch } from '@a-vue'
98
+ import { Editor, EditorContent } from '@tiptap/vue-2'
99
+ import StarterKit from '@tiptap/starter-kit'
100
+ import {
101
+ mdiFormatBold,
102
+ mdiFormatItalic,
103
+ mdiFormatStrikethroughVariant,
104
+ mdiFormatHeader1,
105
+ mdiFormatHeader2,
106
+ mdiFormatListBulleted,
107
+ mdiFormatListNumbered,
108
+ mdiFormatQuoteClose,
109
+ mdiRotateLeft,
110
+ mdiRotateRight
111
+ } from '@mdi/js'
112
+
113
+ @Component({
114
+ props: ['value', 'validator'],
115
+ components: {
116
+ EditorContent
117
+ }
118
+ })
119
+ export default class ARichTextArea extends Vue {
120
+ editor = null
121
+ internalValue = null
122
+ focus = false
123
+
124
+ boldIcon = mdiFormatBold
125
+ italicIcon = mdiFormatItalic
126
+ strikeIcon = mdiFormatStrikethroughVariant
127
+ h1Icon = mdiFormatHeader1
128
+ h2Icon = mdiFormatHeader2
129
+ ulIcon = mdiFormatListBulleted
130
+ olIcon = mdiFormatListNumbered
131
+ commentIcon = mdiFormatQuoteClose
132
+ undoIcon = mdiRotateLeft
133
+ redoIcon = mdiRotateRight
134
+
135
+ created () {
136
+ this.internalValue = this.value
137
+ }
138
+
139
+ mounted () {
140
+ if (this.validator) {
141
+ this.$refs.input.validate(true)
142
+ }
143
+
144
+ this.editor = new Editor({
145
+ content: this.internalValue,
146
+ extensions: [
147
+ StarterKit
148
+ ],
149
+ onUpdate: () => {
150
+ this.$emit('input', this.editor.getHTML())
151
+ },
152
+ onFocus: ({ editor, event }) => {
153
+ this.focus = true
154
+ },
155
+ onBlur: ({ editor, event }) => {
156
+ this.focus = false
157
+ }
158
+ })
159
+
160
+ this.editor.commands.setContent(this.internalValue, false)
161
+ }
162
+
163
+ beforeDestroy () {
164
+ this.editor.destroy()
165
+ }
166
+
167
+ @Watch('value')
168
+ valueChanged () {
169
+ this.internalValue = this.value
170
+
171
+ const isSame = this.editor.getHTML() === this.internalValue
172
+ if (!isSame) {
173
+ this.editor.commands.setContent(this.internalValue, false)
174
+ }
175
+ }
176
+
177
+ get validationRules () {
178
+ const label = this.$attrs.label
179
+ return (this.validator && this.validator.getRules(label)) || []
180
+ }
181
+
182
+ get counter () {
183
+ if (!this.validator) {
184
+ return false
185
+ }
186
+ return this.validator.getParams().max || false
187
+ }
188
+ }
189
+ </script>
190
+
191
+
192
+ <style lang="scss" scoped>
193
+ .v-input:not(.v-input--is-focused) ::v-deep .v-counter {
194
+ display: none;
195
+ }
196
+
197
+ .a-rich-text-editor {
198
+ ::v-deep .ProseMirror {
199
+ &-focused {
200
+ outline: none;
201
+ }
202
+ }
203
+ }
204
+
205
+ .menu-bar {
206
+ margin: -.2rem 0 .5rem -.2rem;
207
+ }
208
+
209
+ .menu-button {
210
+ padding: 0 !important;
211
+ width: 30px !important;
212
+ height: 32px !important;
213
+ min-width: unset !important;
214
+ text-align: center;
215
+ font-size: 1rem;
216
+ background: white !important;
217
+ border: none;
218
+ box-shadow: none;
219
+ border-radius: 0;
220
+
221
+ ::v-deep .v-icon {
222
+ font-size: 20px;
223
+ width: 20px;
224
+ height: 20px;
225
+ }
226
+
227
+ &.strike {
228
+ ::v-deep .v-icon {
229
+ width: 15px;
230
+ }
231
+ }
232
+
233
+ svg {
234
+ width: unset;
235
+ }
236
+
237
+ &.is-active {
238
+ background: #ECECEC !important;
239
+ }
240
+ }
241
+
242
+ ::v-deep .ProseMirror {
243
+ min-height: 200px;
244
+
245
+ > :last-child {
246
+ margin: 0;
247
+ }
248
+
249
+ li p {
250
+ margin: 0;
251
+ }
252
+
253
+ ul {
254
+ margin: 16px 0;
255
+ }
256
+ }
257
+ </style>
@@ -16,6 +16,7 @@
16
16
  <div
17
17
  v-if="isOpen"
18
18
  class="controls"
19
+ :style="cwm_widthStyle"
19
20
  >
20
21
  <div class="background elevation-6" />
21
22
 
@@ -54,6 +55,7 @@
54
55
  :filters.sync="filters"
55
56
  :count.sync="count"
56
57
  :isLoading.sync="isLoading"
58
+ :style="cwm_widthStyle"
57
59
  >
58
60
  <template #header>
59
61
  <div />
@@ -62,19 +64,11 @@
62
64
  </template>
63
65
 
64
66
  <template #row="{ model }">
65
- <v-icon
66
- :color="model.getIcon().color"
67
- size="1.5rem"
68
- v-text="model.getIcon().icon"
69
- />
70
-
71
67
  <slot
72
68
  name="row"
73
69
  :model="model"
74
70
  :on="{ click: selectHandler(model) }"
75
71
  />
76
-
77
- <div class="lastColumn" />
78
72
  </template>
79
73
 
80
74
  <template #not-found="{ filters }">
@@ -98,6 +92,7 @@ import { FilterSourceType } from '@a-vue/components/list/FilterSourceType'
98
92
  import SearchSelectFilters from './search-select/SearchSelectFilters'
99
93
  import SearchSelectList from './search-select/SearchSelectList'
100
94
  import { CancelOnEscMixin } from '@a-vue/services/escape/CancelOnEscMixin'
95
+ import { ComponentWidthMixin } from './mixins/ComponentWidthMixin'
101
96
 
102
97
  @Component({
103
98
  props: [
@@ -115,7 +110,7 @@ import { CancelOnEscMixin } from '@a-vue/services/escape/CancelOnEscMixin'
115
110
  SearchSelectList
116
111
  }
117
112
  })
118
- export default class ASearchSelect extends Mixins(UsesPositionServiceMixin, CancelOnEscMixin) {
113
+ export default class ASearchSelect extends Mixins(ComponentWidthMixin, UsesPositionServiceMixin, CancelOnEscMixin) {
119
114
  selectId = randomCssClass(10)
120
115
  isOpen = false
121
116
  items_ = []
@@ -318,20 +313,25 @@ export default class ASearchSelect extends Mixins(UsesPositionServiceMixin, Canc
318
313
  }
319
314
 
320
315
  .controls {
321
- min-width: 400px;
316
+ width: 400px;
322
317
  position: absolute;
323
318
  z-index: 300;
324
319
  display: block;
325
- padding: 0 0.5rem;
320
+ padding: 0 .5rem;
321
+
322
+ ::v-deep .a-row {
323
+ overflow: unset;
324
+ }
326
325
  }
327
326
 
328
327
  .searchSelectList {
329
- min-width: 400px;
328
+ width: 400px;
330
329
  position: absolute;
331
330
  z-index: 301;
332
331
 
333
332
  max-height: 40vh;
334
333
  overflow-y: auto;
334
+ overflow-x: hidden;
335
335
  overscroll-behavior: contain;
336
336
 
337
337
  ::v-deep .a-table-row {
@@ -8,6 +8,7 @@
8
8
  <slot
9
9
  :changed="changed"
10
10
  :valid="valid"
11
+ :model="model"
11
12
  />
12
13
  </v-form>
13
14
  </template>
@@ -15,7 +16,6 @@
15
16
 
16
17
  <script>
17
18
  import { Component, Vue, Watch } from '@a-vue'
18
- import { apiResources } from '@afeefa/api-resources-client'
19
19
 
20
20
  @Component({
21
21
  props: ['model']
@@ -52,9 +52,5 @@ export default class EditForm extends Vue {
52
52
  changedChanged () {
53
53
  this.$emit('update:changed', this.changed)
54
54
  }
55
-
56
- get type () {
57
- return apiResources.getType(this.model.type)
58
- }
59
55
  }
60
56
  </script>
@@ -21,7 +21,7 @@
21
21
 
22
22
  <template #default="{changed, valid}">
23
23
  <a-row
24
- class="mt-8 mb-2 gap-4"
24
+ class="mt-8 mb-1 pb-1 gap-4"
25
25
  right
26
26
  >
27
27
  <v-btn
@@ -31,23 +31,25 @@
31
31
  Schließen
32
32
  </v-btn>
33
33
 
34
- <v-btn
35
- small
36
- :disabled="!changed || !valid"
37
- color="green white--text"
38
- @click="save"
39
- >
40
- Speichern
41
- </v-btn>
34
+ <a-row gap="2">
35
+ <v-btn
36
+ small
37
+ :disabled="!changed || !valid"
38
+ color="green white--text"
39
+ @click="save"
40
+ >
41
+ Speichern
42
+ </v-btn>
42
43
 
43
- <v-btn
44
- v-if="changed"
45
- small
46
- text
47
- @click="reset"
48
- >
49
- Zurücksetzen
50
- </v-btn>
44
+ <v-icon
45
+ v-if="changed"
46
+ small
47
+ text
48
+ @click="reset"
49
+ >
50
+ {{ undoIcon }}
51
+ </v-icon>
52
+ </a-row>
51
53
  </a-row>
52
54
  </template>
53
55
  </edit-form>
@@ -57,6 +59,7 @@
57
59
 
58
60
  <script>
59
61
  import { Component, Vue, Watch } from '@a-vue'
62
+ import { mdiRotateLeft} from '@mdi/js'
60
63
 
61
64
  @Component({
62
65
  props: ['model', 'title', 'show']
@@ -64,6 +67,8 @@ import { Component, Vue, Watch } from '@a-vue'
64
67
  export default class EditModal extends Vue {
65
68
  show_ = false
66
69
 
70
+ undoIcon = mdiRotateLeft
71
+
67
72
  /**
68
73
  * visiblility changes from outside
69
74
  * this will trigger the show_ watcher,
@@ -1,6 +1,5 @@
1
- import { ListAction } from '@a-vue/api-resources/ApiActions'
2
- import { apiResources } from '@afeefa/api-resources-client'
3
1
  import { Component, Vue } from '@a-vue'
2
+ import { ListAction } from '@a-vue/api-resources/ApiActions'
4
3
 
5
4
  @Component({
6
5
  props: ['name', 'label']
@@ -17,7 +16,7 @@ export class FormFieldMixin extends Vue {
17
16
  }
18
17
 
19
18
  get modelType () {
20
- return apiResources.getType(this.model.type)
19
+ return this.$apiResources.getType(this.model.type)
21
20
  }
22
21
 
23
22
  get field () {
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <a-rich-text-area
3
+ v-model="model[name]"
4
+ />
5
+ </template>
6
+
7
+ <script>
8
+ import { Component, Mixins } from '@a-vue'
9
+ import { FormFieldMixin } from '../FormFieldMixin'
10
+
11
+ @Component
12
+ export default class FormFieldRichTextArea extends Mixins(FormFieldMixin) {
13
+ }
14
+ </script>
@@ -5,6 +5,7 @@ import EditModal from './form/EditModal'
5
5
  import FormFieldCheckbox from './form/fields/FormFieldCheckbox'
6
6
  import FormFieldDate from './form/fields/FormFieldDate'
7
7
  import FormFieldRadioGroup from './form/fields/FormFieldRadioGroup'
8
+ import FormFieldRichTextArea from './form/fields/FormFieldRichTextArea'
8
9
  import FormFieldSearchSelect from './form/fields/FormFieldSearchSelect'
9
10
  import FormFieldSelect from './form/fields/FormFieldSelect'
10
11
  import FormFieldSelect2 from './form/fields/FormFieldSelect2'
@@ -19,6 +20,7 @@ Vue.component('EditForm', EditForm)
19
20
  Vue.component('EditModal', EditModal)
20
21
  Vue.component('FormFieldText', FormFieldText)
21
22
  Vue.component('FormFieldTextArea', FormFieldTextArea)
23
+ Vue.component('FormFieldRichTextArea', FormFieldRichTextArea)
22
24
  Vue.component('FormFieldRadioGroup', FormFieldRadioGroup)
23
25
  Vue.component('FormFieldCheckbox', FormFieldCheckbox)
24
26
  Vue.component('FormFieldDate', FormFieldDate)
@@ -92,6 +92,10 @@ export class ListViewMixin extends Vue {
92
92
  this.load()
93
93
  }
94
94
 
95
+ reload () {
96
+ this.load()
97
+ }
98
+
95
99
  resetFilters () {
96
100
  this.listViewModel.resetFilters()
97
101
  }
@@ -17,7 +17,9 @@ import { Component, Mixins } from '@a-vue'
17
17
  import { ListFilterMixin } from '../ListFilterMixin'
18
18
  import { ListAction } from '@a-vue/api-resources/ApiActions'
19
19
 
20
- @Component
20
+ @Component({
21
+ props: ['itemTitle']
22
+ })
21
23
  export default class ListFilterSelect extends Mixins(ListFilterMixin) {
22
24
  items = null
23
25
 
@@ -61,7 +63,7 @@ export default class ListFilterSelect extends Mixins(ListFilterMixin) {
61
63
  return [
62
64
  ...this.createOptions(),
63
65
  ...models.map(model => ({
64
- itemTitle: model.name,
66
+ itemTitle: (this.itemTitle && this.itemTitle(model)) || model.name || model.title,
65
67
  itemValue: model.id
66
68
  }))
67
69
  ]
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="searchSelectList">
2
+ <div :class="['searchSelectList', {isLoading}]">
3
3
  <template v-if="models_.length">
4
4
  <a-table v-bind="$attrs">
5
5
  <a-table-header
@@ -74,6 +74,10 @@ export default class SearchSelectList extends Mixins(ListViewMixin) {
74
74
 
75
75
 
76
76
  <style scoped lang="scss">
77
+ .isLoading {
78
+ opacity: .6;
79
+ }
80
+
77
81
  .notFound {
78
82
  padding: 0 .5rem .3rem;
79
83
  }
package/src/index.js CHANGED
@@ -1,2 +1,4 @@
1
+ import './styles/vue-app.scss'
2
+
1
3
  export { Mixins, Vue, Watch } from 'vue-property-decorator'
2
4
  export { Component } from '@a-vue/components/vue/Component'
@@ -1,6 +1,18 @@
1
1
  import { apiResources } from '@afeefa/api-resources-client'
2
2
 
3
3
  class ApiResourcesPlugin {
4
+ apiResources = apiResources
5
+
6
+ register (models, apis) {
7
+ apiResources
8
+ .registerModels(models)
9
+ .registerApis(apis)
10
+ }
11
+
12
+ schemasLoaded () {
13
+ return apiResources.schemasLoaded()
14
+ }
15
+
4
16
  install (Vue) {
5
17
  Object.defineProperty(Vue.prototype, '$apiResources', {
6
18
  get () {
@@ -0,0 +1,8 @@
1
+ .a-text-input {
2
+ border: 1px solid #CCCCCC;
3
+ padding: .5rem;
4
+
5
+ &-focused {
6
+ outline: 1px solid #9999FF;
7
+ }
8
+ }
@@ -0,0 +1 @@
1
+ @import "forms";
@@ -4,7 +4,6 @@ import './config/components'
4
4
  import { apiResourcesPlugin } from '@a-vue/plugins/api-resources/ApiResourcesPlugin'
5
5
  import { hasOptionsPlugin } from '@a-vue/plugins/has-options/HasOptionsPlugin'
6
6
  import { timeout } from '@a-vue/utils/timeout'
7
- import { apiResources } from '@afeefa/api-resources-client'
8
7
  import Vue from 'vue'
9
8
 
10
9
  import { appConfig } from './config/AppConfig'
@@ -17,9 +16,7 @@ Vue.use(hasOptionsPlugin)
17
16
  Vue.config.productionTip = false
18
17
 
19
18
  export async function bootstrap ({ apis, models, routing, authService, app, components }) {
20
- apiResources
21
- .registerModels(models)
22
- .registerApis(apis)
19
+ apiResourcesPlugin.register(models, apis)
23
20
 
24
21
  appConfig.authService = authService
25
22
  appConfig.app = app
@@ -36,7 +33,7 @@ export async function bootstrap ({ apis, models, routing, authService, app, comp
36
33
 
37
34
  routing(routeConfigPlugin)
38
35
  const router = await routeConfigPlugin.getRouter()
39
- await apiResources.schemasLoaded()
36
+ await apiResourcesPlugin.schemasLoaded()
40
37
 
41
38
  if (authService) {
42
39
  authService.initApp(router)
@@ -0,0 +1,223 @@
1
+ <template>
2
+ <div>
3
+ <a-table
4
+ v-if="selectedItems.length"
5
+ :style="selectedWidthStyle"
6
+ >
7
+ <a-table-header
8
+ v-if="hasHeader"
9
+ small
10
+ >
11
+ <div
12
+ v-for="(column, index) in selectedColumns"
13
+ :key="index"
14
+ >
15
+ {{ column.header }}
16
+ </div>
17
+ </a-table-header>
18
+
19
+ <a-table-row
20
+ v-for="(item, index) in selectedItems"
21
+ :key="item.id"
22
+ :selected="isClicked(item)"
23
+ >
24
+ <template v-for="(column, index2) in selectedColumns">
25
+ <v-icon
26
+ v-if="column.icon"
27
+ :key="'icon' + index2"
28
+ :color="column.icon.color"
29
+ class="selectedIcon"
30
+ size="1.5rem"
31
+ v-text="column.icon.icon"
32
+ />
33
+
34
+ <div
35
+ v-else
36
+ :key="'text' + index2"
37
+ class="selectedContent"
38
+ >
39
+ {{ column.text(item) }}
40
+ </div>
41
+ </template>
42
+
43
+ <div>
44
+ <a-context-menu
45
+ :ref="'editItem' + index"
46
+ @open="clickedItem = item"
47
+ @close="clickedItem = null"
48
+ >
49
+ <a-context-menu-item @click="removeItem(item, index)">
50
+ <v-icon>$trashCanIcon</v-icon>
51
+ Löschen
52
+ </a-context-menu-item>
53
+ </a-context-menu>
54
+ </div>
55
+ </a-table-row>
56
+ </a-table>
57
+
58
+ <div v-else>
59
+ {{ isEmptyText }}
60
+ </div>
61
+
62
+ <a-search-select
63
+ :listViewConfig="selectableConfig.listViewConfig"
64
+ :loadOnlyIfKeyword="false"
65
+ :selectedItems="selectableSelectedItems"
66
+ :width="selectableWidth"
67
+ @select="addItem"
68
+ >
69
+ <template #activator>
70
+ <a-icon-button
71
+ icon="$plusIcon"
72
+ :text="selectableConfig.addButtonTitle || 'Hinzufügen'"
73
+ class="mt-4"
74
+ />
75
+ </template>
76
+
77
+ <template #filters>
78
+ <list-filter-row>
79
+ <list-filter-search
80
+ :focus="true"
81
+ maxWidth="100%"
82
+ label="Suche Berater:in"
83
+ />
84
+
85
+ <list-filter-page
86
+ :has="{page_size: false, page_number: true}"
87
+ :totalVisible="0"
88
+ />
89
+ </list-filter-row>
90
+ </template>
91
+
92
+ <template #row="{ model, on }">
93
+ <template
94
+ v-for="(column, index) in selectableColumns"
95
+ v-on="on"
96
+ >
97
+ <v-icon
98
+ v-if="column.icon"
99
+ :key="'icon' + index"
100
+ :color="column.icon.color"
101
+ class="selectableIcon"
102
+ size="1.5rem"
103
+ v-on="on"
104
+ v-text="column.icon.icon"
105
+ />
106
+
107
+ <div
108
+ v-else
109
+ :key="'text' + index"
110
+ class="selectableContent"
111
+ v-on="on"
112
+ >
113
+ {{ column.text(model) }}
114
+ </div>
115
+ </template>
116
+ </template>
117
+ </a-search-select>
118
+ </div>
119
+ </template>
120
+
121
+
122
+ <script>
123
+ import { Component, Vue } from '@a-vue'
124
+
125
+ @Component({
126
+ props: [
127
+ 'value',
128
+ 'selectedConfig',
129
+ 'selectedWidth',
130
+ 'selectableConfig',
131
+ 'selectableWidth'
132
+ ]
133
+ })
134
+ export default class SearchSelectFormField extends Vue {
135
+ clickedItem = null
136
+
137
+ get hasHeader () {
138
+ return this.selectedColumns.some(c => !!c.header)
139
+ }
140
+
141
+ get selectedItems () {
142
+ return this.value
143
+ }
144
+
145
+ get selectableSelectedItems () {
146
+ return this.value.map(this.selectedConfig.selectedToSelectableItem)
147
+ }
148
+
149
+ get isEmptyText () {
150
+ return this.selectedConfig.isEmptyText
151
+ }
152
+
153
+ isClicked (selectedItem) {
154
+ return selectedItem === this.clickedItem
155
+ }
156
+
157
+ get selectedColumns () {
158
+ return this.selectedConfig.columns
159
+ }
160
+
161
+ get selectableColumns () {
162
+ return this.selectableConfig.columns
163
+ }
164
+
165
+ addItem (selectableItem) {
166
+ const selectedItem = this.selectedConfig.newSelectedItem(selectableItem)
167
+ const selectedItems = [...this.value, selectedItem]
168
+ if (this.$listeners.add) {
169
+ this.$emit('add', selectedItems)
170
+ } else {
171
+ this.$emit('input', selectedItems)
172
+ }
173
+ }
174
+
175
+ async removeItem (selectedItem, rowIndex) {
176
+ const removeDialog = this.selectedConfig.removeDialog(selectedItem)
177
+ const dialog = {
178
+ anchor: this.$refs['editItem' + rowIndex], // is array [component]
179
+ ...removeDialog,
180
+ yesButton: 'Löschen',
181
+ yesColor: 'red white--text'
182
+ }
183
+
184
+ const selectedItems = this.value.filter(i => i !== selectedItem)
185
+ if (this.$listeners.remove) {
186
+ this.$emit('remove', selectedItems, dialog)
187
+ } else {
188
+ this.$emit('input', selectedItems)
189
+ }
190
+ }
191
+
192
+ get selectedWidthStyle () {
193
+ if (!this.selectedWidth) {
194
+ return ''
195
+ }
196
+
197
+ let width = this.selectedWidth
198
+ if (!isNaN(width)) {
199
+ width = width + 'px'
200
+ }
201
+ return `width: ${width};`
202
+ }
203
+ }
204
+ </script>
205
+
206
+
207
+ <style scoped lang="scss">
208
+ .selectedIcon::v-deep {
209
+ padding-right: .3rem !important;
210
+ }
211
+
212
+ .selectableIcon::v-deep {
213
+ padding-right: 2rem !important;
214
+ }
215
+
216
+ .selectedContent::v-deep {
217
+ width: 100%;
218
+ }
219
+
220
+ .selectableContent::v-deep {
221
+ width: 100%;
222
+ }
223
+ </style>
@@ -25,7 +25,6 @@
25
25
 
26
26
  <script>
27
27
  import { Component, Vue } from '@a-vue'
28
- import { apiResources } from '@afeefa/api-resources-client'
29
28
 
30
29
  @Component({
31
30
  props: ['icon', 'iconModelType', 'label']
@@ -37,7 +36,7 @@ export default class DetailProperty extends Vue {
37
36
  }
38
37
 
39
38
  if (this.iconModelType) {
40
- const ModelClass = apiResources.getModelClass(this.iconModelType)
39
+ const ModelClass = this.$apiResources.getModelClass(this.iconModelType)
41
40
  return ModelClass.icon
42
41
  }
43
42
  }
@@ -77,6 +76,5 @@ export default class DetailProperty extends Vue {
77
76
  padding-left: 55px;
78
77
  }
79
78
  }
80
-
81
79
  }
82
80
  </style>
@@ -4,9 +4,10 @@
4
4
  :class="['content', {order}]"
5
5
  @click="toggleSort"
6
6
  >
7
- <div :class="['text', {active}]">
8
- {{ text }}
9
- </div>
7
+ <div
8
+ :class="['text', {active}]"
9
+ v-html="text"
10
+ />
10
11
 
11
12
  <svg
12
13
  v-if="order"
@@ -9,7 +9,15 @@
9
9
  </div>
10
10
 
11
11
  <template v-if="models_.length">
12
- <template v-if="_table">
12
+ <template v-if="$scopedSlots.models">
13
+ <slot
14
+ name="models"
15
+ :models="models_"
16
+ :setFilter="setFilter"
17
+ />
18
+ </template>
19
+
20
+ <template v-else-if="$scopedSlots['model-table']">
13
21
  <a-table>
14
22
  <a-table-header>
15
23
  <div v-if="$has.icon" />
@@ -20,6 +28,7 @@
20
28
  <a-table-row
21
29
  v-for="model in models_"
22
30
  :key="model.id"
31
+ :class="getModelClass(model)"
23
32
  >
24
33
  <v-icon
25
34
  v-if="$has.icon"
@@ -37,10 +46,11 @@
37
46
  </a-table>
38
47
  </template>
39
48
 
40
- <template v-else>
49
+ <template v-else-if="$scopedSlots.model">
41
50
  <div
42
51
  v-for="model in models_"
43
52
  :key="model.id"
53
+ :class="getModelClass(model)"
44
54
  >
45
55
  <slot
46
56
  name="model"
@@ -66,7 +76,7 @@ import { ListViewMixin } from '@a-vue/components/list/ListViewMixin'
66
76
  import { LoadingEvent } from '@a-vue/events'
67
77
 
68
78
  @Component({
69
- props: ['table']
79
+ props: ['modelClass']
70
80
  })
71
81
  export default class ListView extends Mixins(ListViewMixin) {
72
82
  $hasOptions = ['icon']
@@ -83,8 +93,8 @@ export default class ListView extends Mixins(ListViewMixin) {
83
93
  this.$emit('update:isLoading', this.isLoading)
84
94
  }
85
95
 
86
- get _table () {
87
- return this.table !== false
96
+ getModelClass (model) {
97
+ return this.modelClass && this.modelClass(model)
88
98
  }
89
99
 
90
100
  setFilter (name, value) {
@@ -49,7 +49,6 @@
49
49
  <script>
50
50
  import { Component, Mixins } from '@a-vue'
51
51
  import { EditPageMixin } from './EditPageMixin'
52
- import { apiResources } from '@afeefa/api-resources-client'
53
52
 
54
53
  @Component({
55
54
  props: ['icon', 'title', 'createModel', 'listLink']
@@ -86,7 +85,7 @@ export default class CreatePage extends Mixins(EditPageMixin) {
86
85
  return this.title
87
86
  }
88
87
 
89
- const type = apiResources.getType(this.ModelClass.type)
88
+ const type = this.$apiResources.getType(this.ModelClass.type)
90
89
  return type.t('TITLE_NEW')
91
90
  }
92
91
 
@@ -64,8 +64,8 @@ export default class DetailPage extends Vue {
64
64
  removeConfirmed = null
65
65
 
66
66
  created () {
67
- if (!this.$parent.constructor.detailRouteConfig) {
68
- console.warn('<detail-page> owner must provide a static getDetailConfig method.')
67
+ if (!this.$parent.constructor.getDetailRouteConfig) {
68
+ console.warn('<detail-page> owner must provide a static getDetailRouteConfig method.')
69
69
  }
70
70
  this.$emit('model', this.model)
71
71
  }
@@ -80,7 +80,7 @@ export default class DetailPage extends Vue {
80
80
  }
81
81
 
82
82
  get detailConfig () {
83
- return this.$parent.constructor.detailRouteConfig
83
+ return this.$parent.constructor.getDetailRouteConfig(this.$route)
84
84
  }
85
85
 
86
86
  get document () {
@@ -66,7 +66,6 @@
66
66
 
67
67
  <script>
68
68
  import { Component, Mixins, Watch } from '@a-vue'
69
- import { apiResources } from '@afeefa/api-resources-client'
70
69
  import { EditPageMixin } from './EditPageMixin'
71
70
 
72
71
  @Component({
@@ -78,8 +77,8 @@ export default class EditPage extends Mixins(EditPageMixin) {
78
77
  model_ = null
79
78
 
80
79
  created () {
81
- if (!this.$parent.constructor.editRouteConfig) {
82
- console.warn('<edit-page> owner must provide a static editRouteConfig method.')
80
+ if (!this.$parent.constructor.getEditRouteConfig) {
81
+ console.warn('<edit-page> owner must provide a static getEditRouteConfig method.')
83
82
  }
84
83
 
85
84
  this.model_ = this.model
@@ -95,7 +94,7 @@ export default class EditPage extends Mixins(EditPageMixin) {
95
94
  }
96
95
 
97
96
  get editConfig () {
98
- return this.$parent.constructor.editRouteConfig
97
+ return this.$parent.constructor.getEditRouteConfig(this.$route)
99
98
  }
100
99
 
101
100
  get modelUpateAction () {
@@ -123,7 +122,7 @@ export default class EditPage extends Mixins(EditPageMixin) {
123
122
  return this.title
124
123
  }
125
124
 
126
- const type = apiResources.getType(this.ModelClass.type)
125
+ const type = this.$apiResources.getType(this.ModelClass.type)
127
126
  return type.t('TITLE_EMPTY')
128
127
  }
129
128
 
@@ -21,7 +21,6 @@
21
21
 
22
22
  <script>
23
23
  import { Component, Vue } from '@a-vue'
24
- import { apiResources } from '@afeefa/api-resources-client'
25
24
 
26
25
  @Component({
27
26
  props: ['icon', 'title', 'newTitle', 'newLink', 'Model']
@@ -38,7 +37,7 @@ export default class ListPage extends Vue {
38
37
  return this.title
39
38
  }
40
39
 
41
- const type = apiResources.getType(this.Model.type)
40
+ const type = this.$apiResources.getType(this.Model.type)
42
41
  return type.t('TITLE_PLURAL')
43
42
  }
44
43
 
@@ -47,7 +46,7 @@ export default class ListPage extends Vue {
47
46
  return this.newTitle
48
47
  }
49
48
 
50
- const type = apiResources.getType(this.Model.type)
49
+ const type = this.$apiResources.getType(this.Model.type)
51
50
  return type.t('TITLE_SINGULAR')
52
51
  }
53
52
 
@@ -19,11 +19,11 @@ function load (route) {
19
19
  const routeDefinition = route.meta.routeDefinition
20
20
  const Component = routeDefinition.config.detail
21
21
 
22
- if (!Component.detailRouteConfig) {
23
- console.warn('A detail route component must implement a static detailRouteConfig property.')
22
+ if (!Component.getDetailRouteConfig) {
23
+ console.warn('A detail route component must implement a static getDetailRouteConfig property.')
24
24
  }
25
25
 
26
- const detailConfig = Component.detailRouteConfig
26
+ const detailConfig = Component.getDetailRouteConfig(route)
27
27
  const action = detailConfig.action || detailConfig.ModelClass.getAction('get')
28
28
 
29
29
  return new GetAction()
@@ -20,11 +20,11 @@ function load (route) {
20
20
  const routeDefinition = route.meta.routeDefinition
21
21
  const Component = routeDefinition.config.edit
22
22
 
23
- if (!Component.editRouteConfig) {
24
- console.warn('An edit route component must implement a static editRouteConfig property.')
23
+ if (!Component.getEditRouteConfig) {
24
+ console.warn('An edit route component must implement a static getEditRouteConfig property.')
25
25
  }
26
26
 
27
- const editConfig = Component.editRouteConfig
27
+ const editConfig = Component.getEditRouteConfig(route)
28
28
  const action = editConfig.getAction || editConfig.ModelClass.getAction('get')
29
29
 
30
30
  return new GetAction()
@@ -31,11 +31,11 @@ function load (route) {
31
31
  const routeDefinition = route.meta.routeDefinition
32
32
  const Component = routeDefinition.config.list
33
33
 
34
- if (!Component.listViewConfig) {
35
- console.warn('A list route component must implement a static listViewConfig property.')
34
+ if (!Component.getListRouteConfig) {
35
+ console.warn('A list route component must implement a static getListRouteConfig property.')
36
36
  }
37
37
 
38
- const request = new ListViewModel(Component.listViewConfig)
38
+ const request = new ListViewModel(Component.getListRouteConfig(route))
39
39
  // read from next route query string, but do not push
40
40
  // list component will be init with used_filters
41
41
  .filterSource(new NextRouteFilterSource(route), false)
@@ -6,6 +6,7 @@ import {
6
6
  mdiDelete,
7
7
  mdiDotsHorizontal,
8
8
  mdiDotsVertical,
9
+ mdiLock,
9
10
  mdiLogoutVariant,
10
11
  mdiMagnify,
11
12
  mdiPencil,
@@ -32,7 +33,8 @@ export default new Vuetify({
32
33
  pencilIcon: mdiPencil,
33
34
  trashCanIcon: mdiDelete,
34
35
  calendarIcon: mdiCalendar,
35
- searchIcon: mdiMagnify
36
+ searchIcon: mdiMagnify,
37
+ lockIcon: mdiLock
36
38
  }
37
39
  },
38
40
  breakpoint: {
@@ -1,11 +1,17 @@
1
1
  import { Model as ApiResourcesModel, apiResources } from '@afeefa/api-resources-client'
2
2
  import { mdiAlphaMCircle } from '@mdi/js'
3
3
 
4
+ import { ModelAdminConfig } from './ModelAdminConfig'
5
+
6
+ export { ModelAdminConfig }
7
+
4
8
  export class Model extends ApiResourcesModel {
5
9
  static resourceType = null
6
10
  static routeName = null
7
11
  static routeIdKey = 'id'
8
12
 
13
+ static config = null
14
+
9
15
  static getLink (action) {
10
16
  return (new this()).getLink(action)
11
17
  }
@@ -21,6 +27,13 @@ export class Model extends ApiResourcesModel {
21
27
  return null
22
28
  }
23
29
 
30
+ static get adminConfig () {
31
+ return new ModelAdminConfig()
32
+ .setIcon({
33
+ icon: mdiAlphaMCircle
34
+ })
35
+ }
36
+
24
37
  static icon = {
25
38
  icon: mdiAlphaMCircle,
26
39
  color: 'blue lighten-2'
@@ -0,0 +1,20 @@
1
+ export class ModelAdminConfig {
2
+ icon = null
3
+ selectedListConfig = null
4
+ selectableListConfig = null
5
+
6
+ setIcon (icon) {
7
+ this.icon = icon
8
+ return this
9
+ }
10
+
11
+ setSelectedListConfig (selectedListConfig) {
12
+ this.selectedListConfig = selectedListConfig
13
+ return this
14
+ }
15
+
16
+ setSelectableListConfig (selectableListConfig) {
17
+ this.selectableListConfig = selectableListConfig
18
+ return this
19
+ }
20
+ }
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <div>TEST</div>
3
+ </template>
4
+
5
+
6
+ <script>
7
+ import { Component, Vue } from '@a-vue'
8
+
9
+ @Component({
10
+ props: ['name']
11
+ })
12
+ export default class Splash extends Vue {
13
+ }
14
+ </script>
15
+
16
+
17
+ <style lang="scss" scoped>
18
+ </style>