@afeefa/vue-app 0.0.61 → 0.0.62

Sign up to get free protection for your applications and to get access to all the features.
@@ -1 +1 @@
1
- 0.0.61
1
+ 0.0.62
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afeefa/vue-app",
3
- "version": "0.0.61",
3
+ "version": "0.0.62",
4
4
  "description": "",
5
5
  "author": "Afeefa Kollektiv <kollektiv@afeefa.de>",
6
6
  "license": "MIT",
@@ -0,0 +1,87 @@
1
+ import { AlertEvent, LoadingEvent } from '@a-vue/events'
2
+ import { eventBus } from '@a-vue/plugins/event-bus/EventBus'
3
+ import { sleep } from '@a-vue/utils/timeout'
4
+ import { ApiAction as ApiResourcesApiAction } from '@afeefa/api-resources-client'
5
+
6
+ import { SaveEvent } from '../events'
7
+
8
+ export class ApiAction extends ApiResourcesApiAction {
9
+ _dispatchGlobalLoadingEvents = false
10
+ _dispatchGlobalSaveEvents = false
11
+ _minDuration = 100
12
+ _startTime = 0
13
+
14
+ id (id) {
15
+ this.param('id', id)
16
+ return this
17
+ }
18
+
19
+ dispatchGlobalLoadingEvents (dispatch = true) {
20
+ this._dispatchGlobalLoadingEvents = dispatch
21
+ return this
22
+ }
23
+
24
+ dispatchGlobalSaveEvents (dispatch = true) {
25
+ this._dispatchGlobalSaveEvents = dispatch
26
+ return this
27
+ }
28
+
29
+ async beforeRequest () {
30
+ this._startTime = Date.now()
31
+
32
+ if (this._dispatchGlobalLoadingEvents) {
33
+ eventBus.dispatch(new LoadingEvent(LoadingEvent.START_LOADING))
34
+ }
35
+
36
+ if (this._dispatchGlobalSaveEvents) {
37
+ eventBus.dispatch(new SaveEvent(SaveEvent.START_SAVING))
38
+ }
39
+ }
40
+
41
+ async beforeBulkRequest () {
42
+ if (this._dispatchGlobalLoadingEvents) {
43
+ eventBus.dispatch(new LoadingEvent(LoadingEvent.START_LOADING))
44
+ }
45
+
46
+ if (this._dispatchGlobalSaveEvents) {
47
+ eventBus.dispatch(new SaveEvent(SaveEvent.START_SAVING))
48
+ }
49
+ }
50
+
51
+ async afterRequest () {
52
+ if (this._minDuration) {
53
+ const diffTime = Date.now() - this._startTime
54
+ const restTime = Math.max(0, this._minDuration - diffTime)
55
+ if (restTime) {
56
+ await sleep(restTime / 1000)
57
+ }
58
+ }
59
+
60
+ if (this._dispatchGlobalLoadingEvents) {
61
+ eventBus.dispatch(new LoadingEvent(LoadingEvent.STOP_LOADING))
62
+ }
63
+
64
+ if (this._dispatchGlobalSaveEvents) {
65
+ eventBus.dispatch(new SaveEvent(SaveEvent.STOP_SAVING))
66
+ }
67
+ }
68
+
69
+ async afterBulkRequest () {
70
+ if (this._dispatchGlobalLoadingEvents) {
71
+ eventBus.dispatch(new LoadingEvent(LoadingEvent.STOP_LOADING))
72
+ }
73
+
74
+ if (this._dispatchGlobalSaveEvents) {
75
+ eventBus.dispatch(new SaveEvent(SaveEvent.STOP_SAVING))
76
+ }
77
+ }
78
+
79
+ alert (message) {
80
+ eventBus.dispatch(new AlertEvent(AlertEvent.MESSAGE, {
81
+ message
82
+ }))
83
+ }
84
+ }
85
+
86
+ export class BulkAction extends ApiAction {
87
+ }
@@ -0,0 +1,13 @@
1
+ import { BulkAction } from './ApiAction'
2
+ import { DeleteAction } from './DeleteAction'
3
+ import { GetAction } from './GetAction'
4
+ import { ListAction } from './ListAction'
5
+ import { SaveAction } from './SaveAction'
6
+
7
+ export {
8
+ BulkAction,
9
+ ListAction,
10
+ GetAction,
11
+ SaveAction,
12
+ DeleteAction
13
+ }
@@ -0,0 +1,21 @@
1
+ import { ApiAction } from './ApiAction'
2
+
3
+ export class DeleteAction extends ApiAction {
4
+ _minDuration = 400
5
+
6
+ delete () {
7
+ this.data(null)
8
+
9
+ return this.execute()
10
+ }
11
+
12
+ async afterRequest () {
13
+ await super.afterRequest()
14
+
15
+ this.alert('Die Daten wurden gelöscht.')
16
+ }
17
+
18
+ processResult () {
19
+ return true
20
+ }
21
+ }
@@ -0,0 +1,18 @@
1
+ import { AlertEvent } from '@a-vue/events'
2
+ import { eventBus } from '@a-vue/plugins/event-bus/EventBus'
3
+
4
+ import { ApiAction } from './ApiAction'
5
+
6
+ export class GetAction extends ApiAction {
7
+ load () {
8
+ return this.execute()
9
+ }
10
+
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
+ }))
17
+ }
18
+ }
@@ -0,0 +1,27 @@
1
+ import { NextRouteFilterSource } from '@a-vue/components/list/NextRouteFilterSource'
2
+ import { ListViewModel } from '@afeefa/api-resources-client'
3
+
4
+ import { ApiAction } from './ApiAction'
5
+
6
+ export class ListAction extends ApiAction {
7
+ load () {
8
+ return this.execute()
9
+ }
10
+
11
+ initFiltersForRoute (route) {
12
+ const request = new ListViewModel(this)
13
+ // read from next route query string, but do not push
14
+ // list component will be init with used_filters
15
+ .filterSource(new NextRouteFilterSource(route), false)
16
+ // read from history, but do not push
17
+ // list component will be init with used_filters
18
+ .historyKey(route.path, false)
19
+ .initFilters({
20
+ source: true,
21
+ history: true
22
+ })
23
+ .getApiRequest()
24
+
25
+ return ListAction.fromRequest(request)
26
+ }
27
+ }
@@ -0,0 +1,15 @@
1
+ import { ApiAction } from './ApiAction'
2
+
3
+ export class SaveAction extends ApiAction {
4
+ _minDuration = 400
5
+
6
+ save () {
7
+ return this.execute()
8
+ }
9
+
10
+ async afterRequest () {
11
+ await super.afterRequest()
12
+
13
+ this.alert('Die Daten wurden gespeichert.')
14
+ }
15
+ }
@@ -45,7 +45,7 @@
45
45
  <div :class="listCssClass">
46
46
  <search-select-list
47
47
  v-if="isOpen"
48
- :listViewConfig="listViewConfig"
48
+ :listAction="listAction"
49
49
  :q="q"
50
50
  :selectedItems="selectedItems"
51
51
  :events="false"
@@ -96,7 +96,7 @@ import { ComponentWidthMixin } from './mixins/ComponentWidthMixin'
96
96
 
97
97
  @Component({
98
98
  props: [
99
- 'listViewConfig',
99
+ 'listAction',
100
100
  'q',
101
101
  'width',
102
102
  'closeOnSelect',
@@ -3,12 +3,10 @@
3
3
  v-model="valid"
4
4
  autocomplete="off"
5
5
  >
6
- <slot name="fields" />
7
-
8
6
  <slot
9
7
  :changed="changed"
10
8
  :valid="valid"
11
- :model="model"
9
+ :model="modelToEdit"
12
10
  />
13
11
  </v-form>
14
12
  </template>
@@ -18,14 +16,31 @@
18
16
  import { Component, Vue, Watch } from '@a-vue'
19
17
 
20
18
  @Component({
21
- props: ['model']
19
+ props: [
20
+ 'model',
21
+ 'createModelToEdit'
22
+ ]
22
23
  })
23
24
  export default class EditForm extends Vue {
24
25
  EDIT_FORM = true
25
26
 
27
+ modelToEdit = null
26
28
  valid = false
27
29
  lastJson = null
28
30
 
31
+ created () {
32
+ this.reset()
33
+ }
34
+
35
+ reset () {
36
+ if (this.createModelToEdit) {
37
+ this.modelToEdit = this.createModelToEdit(this.model)
38
+ } else {
39
+ this.modelToEdit = this.model
40
+ }
41
+ this.lastJson = this.json
42
+ }
43
+
29
44
  /**
30
45
  * This will be triggered after the this.model has been set
31
46
  * but before sub components may have changed model values
@@ -33,14 +48,20 @@ export default class EditForm extends Vue {
33
48
  * Using the created() method would result in already having set
34
49
  * the default date, hence not detecting a valid "change" anymore.
35
50
  */
36
- @Watch('model', {immediate: true})
51
+ // @Watch('modelToEdit', {immediate: true})
52
+ // @Watch('modelToEdit')
53
+ // modelToEditChanged () {
54
+ // this.lastJson = this.json
55
+ // this.$emit('update:changed', this.changed)
56
+ // }
57
+
58
+ @Watch('model')
37
59
  modelChanged () {
38
- this.lastJson = this.json
39
- this.$emit('update:changed', this.changed)
60
+ this.reset()
40
61
  }
41
62
 
42
63
  get json () {
43
- return JSON.stringify(this.model)
64
+ return JSON.stringify(this.modelToEdit)
44
65
  }
45
66
 
46
67
  get changed () {
@@ -9,7 +9,7 @@ export class FormFieldMixin extends Vue {
9
9
  let parent = this
10
10
  while (parent) {
11
11
  if (parent.EDIT_FORM) {
12
- return parent.model
12
+ return parent.modelToEdit
13
13
  }
14
14
  parent = parent.$parent
15
15
  }
@@ -8,7 +8,7 @@ import { FilterSourceType } from './FilterSourceType'
8
8
  @Component({
9
9
  props: [
10
10
  'models', 'meta', // given, if already loaded
11
- 'listViewConfig',
11
+ 'listAction',
12
12
  'filterHistoryKey',
13
13
  'loadOnlyIfKeyword',
14
14
  {
@@ -52,7 +52,7 @@ export class ListViewMixin extends Vue {
52
52
  this.meta_ = this.meta
53
53
  }
54
54
 
55
- this.listViewModel = new ListViewModel(this.listViewConfig)
55
+ this.listViewModel = new ListViewModel(this.listAction)
56
56
  .filterSource(filterSource, !!filterSource)
57
57
  .historyKey(historyKey, this.history)
58
58
  .usedFilters(this.meta_.used_filters || null, this.meta_.count_search || 0)
@@ -8,12 +8,20 @@
8
8
  import { Component, Vue } from '@a-vue'
9
9
 
10
10
  @Component({
11
- props: ['prepend']
11
+ props: {
12
+ prepend: {
13
+ type: Boolean
14
+ }
15
+ }
12
16
  })
13
17
  export default class AppBarButton extends Vue {
14
18
  mounted () {
15
19
  const section = this.getButtonBar()
16
- section.appendChild(this.$el)
20
+ if (this.prepend) {
21
+ section.prepend(this.$el)
22
+ } else {
23
+ section.append(this.$el)
24
+ }
17
25
  }
18
26
 
19
27
  destroyed () {
@@ -60,7 +60,7 @@
60
60
  </div>
61
61
 
62
62
  <a-search-select
63
- :listViewConfig="selectableConfig.listViewConfig"
63
+ :listAction="selectableConfig.listAction"
64
64
  :loadOnlyIfKeyword="false"
65
65
  :selectedItems="selectableSelectedItems"
66
66
  :width="selectableWidth"
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <a-row gap="2">
3
+ <v-btn
4
+ :small="small"
5
+ :disabled="!changed || !valid"
6
+ color="green white--text"
7
+ @click="$emit('save')"
8
+ >
9
+ Speichern
10
+ </v-btn>
11
+
12
+ <v-icon
13
+ v-if="changed"
14
+ :small="small"
15
+ text
16
+ title="Formular zurücksetzen"
17
+ @click="$emit('reset')"
18
+ >
19
+ {{ undoIcon }}
20
+ </v-icon>
21
+ </a-row>
22
+ </template>
23
+
24
+ <script>
25
+ import { Component, Vue } from '@a-vue'
26
+ import { mdiRotateLeft} from '@mdi/js'
27
+
28
+ @Component({
29
+ props: [
30
+ 'changed',
31
+ 'valid',
32
+ 'small'
33
+ ]
34
+ })
35
+ export default class EditFormButtons extends Vue {
36
+ undoIcon = mdiRotateLeft
37
+ }
38
+ </script>
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <div>
3
+ <a-icon-button
4
+ small
5
+ color="red white--text"
6
+ :class="'removeButton-' + dialogId"
7
+ icon="$trashCanIcon"
8
+ text="Löschen"
9
+ @click="remove"
10
+ />
11
+
12
+ <a-dialog
13
+ :id="dialogId"
14
+ :anchor="[document, '.removeButton-' + dialogId]"
15
+ :active="protect ? removeKey === removeConfirmed : true"
16
+ >
17
+ <template v-if="protect">
18
+ <div>Bitte folgenden Key eingeben: <strong class="removeKey">{{ removeKey }}</strong></div>
19
+
20
+ <a-text-field
21
+ v-model="removeConfirmed"
22
+ label="Key"
23
+ :focus="true"
24
+ width="100"
25
+ />
26
+ </template>
27
+ </a-dialog>
28
+ </div>
29
+ </template>
30
+
31
+
32
+ <script>
33
+ import { Component, Vue } from '@a-vue'
34
+ import { DialogEvent } from '@a-vue/events'
35
+ import { randomCssClass } from '@a-vue/utils/random'
36
+
37
+ @Component({
38
+ props: ['itemTitle', 'protect']
39
+ })
40
+ export default class EditPage extends Vue {
41
+ dialogId = randomCssClass(10)
42
+ document = document
43
+ removeKey = null
44
+ removeConfirmed = null
45
+
46
+ async remove () {
47
+ if (this.protect) {
48
+ this.removeKey = [...Array(6)].map(() => Math.floor(Math.random() * 16).toString(16)).join('')
49
+ }
50
+
51
+ const result = await this.$events.dispatch(new DialogEvent(DialogEvent.SHOW, {
52
+ id: this.dialogId,
53
+ title: this.itemTitle + ' löschen?',
54
+ message: 'Soll ' + this.itemTitle + ' gelöscht werden?',
55
+ yesButton: 'Löschen',
56
+ yesColor: 'red white--text'
57
+ }))
58
+
59
+ if (result === DialogEvent.RESULT_YES) {
60
+ this.$emit('remove')
61
+ }
62
+ }
63
+ }
64
+ </script>
65
+
66
+
67
+ <style lang="scss" scoped>
68
+ .removeKey {
69
+ user-select: none;
70
+ }
71
+ </style>
@@ -7,7 +7,8 @@ import DetailContent from './detail/DetailContent'
7
7
  import DetailMeta from './detail/DetailMeta'
8
8
  import DetailProperty from './detail/DetailProperty'
9
9
  import DetailTitle from './detail/DetailTitle'
10
- import Start from './Start.vue'
10
+ import EditFormButtons from './form/EditFormButtons'
11
+ import RemoveButton from './form/RemoveButton'
11
12
  import ListCard from './list/ListCard'
12
13
  import ListColumnHeader from './list/ListColumnHeader'
13
14
  import ListContent from './list/ListContent'
@@ -20,6 +21,7 @@ import CreatePage from './pages/CreatePage'
20
21
  import DetailPage from './pages/DetailPage'
21
22
  import EditPage from './pages/EditPage'
22
23
  import ListPage from './pages/ListPage'
24
+ import Start from './Start.vue'
23
25
 
24
26
  Vue.component('ListCard', ListCard)
25
27
  Vue.component('ListColumnHeader', ListColumnHeader)
@@ -32,6 +34,8 @@ Vue.component('ListPage', ListPage)
32
34
  Vue.component('CreatePage', CreatePage)
33
35
 
34
36
  Vue.component('EditPage', EditPage)
37
+ Vue.component('EditFormButtons', EditFormButtons)
38
+ Vue.component('RemoveButton', RemoveButton)
35
39
 
36
40
  Vue.component('ModelCount', ModelCount)
37
41
  Vue.component('ModelIcon', ModelIcon)
@@ -1,151 +1,55 @@
1
1
  <template>
2
- <div class="editPage">
3
- <app-bar-title
4
- :icon="_icon"
5
- :title="_title"
6
- />
7
-
8
- <app-bar-button v-if="$has.list">
9
- <v-btn
10
- :to="_listLink"
11
- small
12
- >
13
- Liste
14
- </v-btn>
15
- </app-bar-button>
16
-
17
- <app-bar-button v-if="$has.detail">
18
- <v-btn
19
- :to="model.getLink()"
20
- small
21
- >
22
- Ansehen
23
- </v-btn>
24
- </app-bar-button>
25
-
26
- <edit-form
27
- :model="modelToEdit"
28
- :valid.sync="valid"
29
- :changed.sync="changed"
30
- >
31
- <template #fields>
32
- <slot
33
- name="fields"
34
- :model="modelToEdit"
35
- />
36
- </template>
37
- </edit-form>
38
-
39
- <a-row class="mt-8">
40
- <v-btn
41
- :disabled="!changed || !valid"
42
- color="green white--text"
43
- @click="save"
44
- >
45
- Speichern
46
- </v-btn>
47
-
48
- <v-btn
49
- v-if="changed"
50
- text
51
- @click="reset"
52
- >
53
- Zurücksetzen
54
- </v-btn>
55
- </a-row>
56
- </div>
2
+ <edit-form
3
+ ref="form"
4
+ :model="model"
5
+ :createModelToEdit="createModelToEdit"
6
+ >
7
+ <template #default="{model, changed, valid}">
8
+ <slot
9
+ :model="model"
10
+ :changed="changed"
11
+ :valid="valid"
12
+ />
13
+
14
+ <edit-form-buttons
15
+ :changed="changed"
16
+ :valid="valid"
17
+ @save="$emit('save', model)"
18
+ @reset="$refs.form.reset()"
19
+ />
20
+ </template>
21
+ </edit-form>
57
22
  </template>
58
23
 
59
24
  <script>
60
- import { Component, Mixins, Watch } from '@a-vue'
61
- import { EditPageMixin } from './EditPageMixin'
25
+ import { Component, Vue } from '@a-vue'
26
+ import { DialogEvent } from '@a-vue/events'
62
27
 
63
28
  @Component({
64
- props: [
65
- 'model',
66
- 'icon',
67
- 'title',
68
- 'listLink',
69
- 'getAction'
70
- ]
29
+ props: ['model', 'createModelToEdit']
71
30
  })
72
- export default class EditPage extends Mixins(EditPageMixin) {
73
- $hasOptions = ['detail', {list: false}]
74
-
75
- model_ = null
31
+ export default class EditPage extends Vue {
32
+ unregisterRouterHook = null
76
33
 
77
34
  created () {
78
- if (!this.$parent.constructor.getEditRouteConfig) {
79
- console.warn('<edit-page> owner must provide a static getEditRouteConfig method.')
80
- }
81
-
82
- this.model_ = this.model
83
- this.reset()
84
- }
85
-
86
- @Watch('model')
87
- modelChanged () {
88
- this.model_ = this.model
89
- this.reset()
90
- }
91
-
92
- get editConfig () {
93
- return this.$parent.constructor.getEditRouteConfig(this.$route)
94
- }
95
-
96
- get modelUpateAction () {
97
- return this.editConfig.updateAction || this.ModelClass.getAction('save')
98
- }
99
-
100
- get _getAction () {
101
- if (this.getAction) {
102
- return this.getAction
103
- }
104
- return this.ModelClass.getAction('get')
105
- }
106
-
107
- get _icon () {
108
- return this.icon || this.model.getIcon()
109
- }
110
-
111
- get _title () {
112
- const title = this.modelToEdit.getTitle()
113
- if (title) {
114
- return title
115
- }
116
-
117
- if (this.title) {
118
- return this.title
119
- }
120
-
121
- return this.$t('Admin.Types', this.ModelClass.type, null, 'TITLE_EMPTY', 'de')
122
- }
123
-
124
- get _listLink () {
125
- if (this.listLink) {
126
- if (typeof this.listLink === 'function') {
127
- return this.listLink()
128
- } else {
129
- return this.listLink
35
+ this.unregisterRouterHook = this.$router.beforeEach(async (to, from, next) => {
36
+ if (this.$refs.form.changed) {
37
+ const result = await this.$events.dispatch(new DialogEvent(DialogEvent.SHOW, {
38
+ title: 'Änderungen verwerfen?',
39
+ message: 'Im Formular sind nicht gespeicherte Änderungen. Sollen diese verworfen werden?',
40
+ yesButton: 'Verwerfen'
41
+ }))
42
+ if (result === DialogEvent.RESULT_YES) {
43
+ next()
44
+ }
45
+ return
130
46
  }
131
- }
132
- return this.model.getLink('list')
133
- }
134
-
135
- createModelToEdit () {
136
- return this.model_.cloneForEdit(this.fields)
137
- }
138
-
139
- get saveParams () {
140
- return {
141
- id: this.model.id
142
- }
47
+ next()
48
+ })
143
49
  }
144
50
 
145
- afterSave (model) {
146
- this.model_ = model
147
- this.reset()
148
- this.$emit('saved', model)
51
+ destroyed () {
52
+ this.unregisterRouterHook()
149
53
  }
150
54
  }
151
55
  </script>
@@ -0,0 +1,75 @@
1
+ import { Component, Vue, Watch } from '@a-vue'
2
+
3
+ Component.registerHooks([
4
+ 'beforeRouteEnter',
5
+ 'beforeRouteUpdate'
6
+ ])
7
+
8
+ let onLoadCallback = () => {}
9
+ let routerHookCalled = false
10
+
11
+ function onLoad (callback) {
12
+ onLoadCallback = callback
13
+ }
14
+
15
+ function load (route) {
16
+ const Component = [...route.matched].pop().components.default
17
+
18
+ if (!Component.drm_getActions) {
19
+ console.warn('A data route component must implement a static drm_getActions() method.')
20
+ }
21
+
22
+ const request = Component.drm_getActions(route, onLoad)
23
+ if (Array.isArray(request)) {
24
+ return Promise.all(request.map(request => {
25
+ return request
26
+ .dispatchGlobalLoadingEvents()
27
+ .execute()
28
+ }))
29
+ } else {
30
+ return request
31
+ .dispatchGlobalLoadingEvents()
32
+ .execute()
33
+ }
34
+ }
35
+
36
+ @Component
37
+ export class DataRouteMixin extends Vue {
38
+ drm_isLoaded = false
39
+
40
+ async beforeRouteEnter (to, from, next) {
41
+ routerHookCalled = true
42
+ const result = await load(to)
43
+
44
+ next(vm => {
45
+ if (result !== false) {
46
+ vm.drm_onLoad(result)
47
+ vm.drm_isLoaded = true
48
+ }
49
+ routerHookCalled = false
50
+ })
51
+ }
52
+
53
+ /**
54
+ * triggered both, if route name or route params change
55
+ */
56
+ @Watch('$route.params')
57
+ async routeParamsChanged () {
58
+ if (routerHookCalled) {
59
+ return
60
+ }
61
+
62
+ if (!this.drm_isLoaded) {
63
+ const result = await load(this.$route)
64
+
65
+ if (result !== false) {
66
+ this.drm_onLoad(result)
67
+ this.drm_isLoaded = true
68
+ }
69
+ }
70
+ }
71
+
72
+ drm_onLoad (result) {
73
+ onLoadCallback(this, result)
74
+ }
75
+ }
@@ -17,11 +17,11 @@ export class Model extends ApiResourcesModel {
17
17
  return (new this()).getLink(action)
18
18
  }
19
19
 
20
- static getAction (action) {
20
+ static getAction (actionName) {
21
21
  if (this.resourceType) {
22
22
  return apiResources.getAction({
23
- resource: this.resourceType,
24
- action
23
+ resourceType: this.resourceType,
24
+ actionName
25
25
  })
26
26
  }
27
27
  console.warn('You can\'t get an action out of a model without resourceType:', this.type)