@afeefa/vue-app 0.0.54 → 0.0.57

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