@afeefa/vue-app 0.0.61 → 0.0.64

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. package/.afeefa/package/release/version.txt +1 -1
  2. package/README.md +3 -1
  3. package/package.json +1 -1
  4. package/src/api-resources/ApiAction.js +87 -0
  5. package/src/api-resources/ApiActions2.js +13 -0
  6. package/src/api-resources/DeleteAction.js +21 -0
  7. package/src/api-resources/GetAction.js +18 -0
  8. package/src/api-resources/ListAction.js +27 -0
  9. package/src/api-resources/SaveAction.js +15 -0
  10. package/src/components/AModal.vue +21 -3
  11. package/src/components/ASearchSelect.vue +2 -2
  12. package/src/components/ATableRow.vue +4 -0
  13. package/src/components/flying-context/FlyingContextEvent.js +5 -0
  14. package/src/components/form/EditForm.vue +30 -8
  15. package/src/components/form/EditModal.vue +50 -35
  16. package/src/components/form/FormFieldMixin.js +1 -1
  17. package/src/components/form/NestedEditForm.vue +4 -0
  18. package/src/components/list/ListViewMixin.js +2 -2
  19. package/src/events.js +1 -0
  20. package/src-admin/bootstrap.js +1 -0
  21. package/src-admin/components/App.vue +5 -1
  22. package/src-admin/components/FlyingContext.vue +77 -0
  23. package/src-admin/components/FlyingContextContainer.vue +85 -0
  24. package/src-admin/components/app/AppBarButton.vue +10 -2
  25. package/src-admin/components/controls/SearchSelectFormField.vue +1 -1
  26. package/src-admin/components/form/EditFormButtons.vue +40 -0
  27. package/src-admin/components/form/RemoveButton.vue +71 -0
  28. package/src-admin/components/index.js +7 -8
  29. package/src-admin/components/list/ListView.vue +19 -5
  30. package/src-admin/components/pages/EditPage.vue +48 -134
  31. package/src-admin/components/routes/DataRouteMixin.js +84 -0
  32. package/src-admin/config/routing.js +0 -11
  33. package/src-admin/directives/index.js +26 -0
  34. package/src-admin/models/Model.js +3 -3
  35. package/src-admin/components/pages/CreatePage.vue +0 -114
  36. package/src-admin/components/pages/DetailPage.vue +0 -143
  37. package/src-admin/components/pages/EditPageMixin.js +0 -96
  38. package/src-admin/components/pages/ListPage.vue +0 -55
  39. package/src-admin/components/routes/CreateRoute.vue +0 -15
  40. package/src-admin/components/routes/DetailRoute.vue +0 -85
  41. package/src-admin/components/routes/EditRoute.vue +0 -78
  42. package/src-admin/components/routes/ListRoute.vue +0 -110
@@ -0,0 +1,85 @@
1
+ <template>
2
+ <div
3
+ id="flyingContextContainer"
4
+ :class="{visible}"
5
+ >
6
+ <a
7
+ href=""
8
+ @click.prevent="hide"
9
+ >close</a>
10
+
11
+ <br>
12
+ <br>
13
+
14
+ <div id="flyingContextContainer__children" />
15
+ </div>
16
+ </template>
17
+
18
+ <script>
19
+ import { Component, Vue } from '@a-vue'
20
+ import { FlyingContextEvent } from '@a-vue/events'
21
+
22
+ @Component({
23
+ props: []
24
+ })
25
+ export default class FlyingContextContainer extends Vue {
26
+ visible = false
27
+
28
+ mounted () {
29
+ this.mutationWatcher = new MutationObserver(this.domChanged)
30
+ this.mutationWatcher.observe(this.getChildrenContainer(), { childList: true })
31
+
32
+ window.addEventListener('mousedown', this.onClickOutside)
33
+ }
34
+
35
+ domChanged () {
36
+ const container = this.getChildrenContainer()
37
+ this.visible = !!container.children.length
38
+ }
39
+
40
+ hide () {
41
+ if (this.visible) {
42
+ this.$events.dispatch(new FlyingContextEvent(FlyingContextEvent.HIDE_ALL))
43
+ }
44
+ }
45
+
46
+ getChildrenContainer () {
47
+ return this.$el.querySelector('#flyingContextContainer__children')
48
+ }
49
+
50
+ onClickOutside (e) {
51
+ // check if trigger is clicked
52
+ let parent = e.target
53
+ while (parent) {
54
+ if (parent.classList.contains('flyingContextTrigger')) {
55
+ return
56
+ }
57
+ parent = parent.parentElement
58
+ }
59
+
60
+ // check if flying context ist clicked
61
+ if (!this.$el.contains(e.target)) {
62
+ this.hide()
63
+ }
64
+ }
65
+ }
66
+ </script>
67
+
68
+
69
+ <style lang="scss" scoped>
70
+ #flyingContextContainer {
71
+ position: fixed;
72
+ z-index: 200;
73
+ right: 0;
74
+ width: 60vw;
75
+ height: 80vh;
76
+ top: 10vh;
77
+ background: rgba($color: white, $alpha: .6);
78
+ border: 1px solid black;
79
+ transition: right .2s;
80
+
81
+ &:not(.visible) {
82
+ right: -80vw;
83
+ }
84
+ }
85
+ </style>
@@ -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,40 @@
1
+ <template>
2
+ <a-row gap="2">
3
+ <v-btn
4
+ :small="small"
5
+ :disabled="($has.reset && !changed) || !valid"
6
+ color="green white--text"
7
+ @click="$emit('save')"
8
+ >
9
+ Speichern
10
+ </v-btn>
11
+
12
+ <v-icon
13
+ v-if="$has.reset && 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
+ $hasOptions = ['reset']
37
+
38
+ undoIcon = mdiRotateLeft
39
+ }
40
+ </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,9 @@ 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 FlyingContext from './FlyingContext.vue'
11
+ import EditFormButtons from './form/EditFormButtons'
12
+ import RemoveButton from './form/RemoveButton'
11
13
  import ListCard from './list/ListCard'
12
14
  import ListColumnHeader from './list/ListColumnHeader'
13
15
  import ListContent from './list/ListContent'
@@ -16,10 +18,8 @@ import ListTitle from './list/ListTitle'
16
18
  import ListView from './list/ListView'
17
19
  import ModelCount from './model/ModelCount'
18
20
  import ModelIcon from './model/ModelIcon'
19
- import CreatePage from './pages/CreatePage'
20
- import DetailPage from './pages/DetailPage'
21
21
  import EditPage from './pages/EditPage'
22
- import ListPage from './pages/ListPage'
22
+ import Start from './Start.vue'
23
23
 
24
24
  Vue.component('ListCard', ListCard)
25
25
  Vue.component('ListColumnHeader', ListColumnHeader)
@@ -27,11 +27,10 @@ Vue.component('ListContent', ListContent)
27
27
  Vue.component('ListMeta', ListMeta)
28
28
  Vue.component('ListTitle', ListTitle)
29
29
  Vue.component('ListView', ListView)
30
- Vue.component('ListPage', ListPage)
31
-
32
- Vue.component('CreatePage', CreatePage)
33
30
 
34
31
  Vue.component('EditPage', EditPage)
32
+ Vue.component('EditFormButtons', EditFormButtons)
33
+ Vue.component('RemoveButton', RemoveButton)
35
34
 
36
35
  Vue.component('ModelCount', ModelCount)
37
36
  Vue.component('ModelIcon', ModelIcon)
@@ -39,7 +38,6 @@ Vue.component('ModelIcon', ModelIcon)
39
38
  Vue.component('DetailContent', DetailContent)
40
39
  Vue.component('DetailMeta', DetailMeta)
41
40
  Vue.component('DetailTitle', DetailTitle)
42
- Vue.component('DetailPage', DetailPage)
43
41
  Vue.component('DetailProperty', DetailProperty)
44
42
  Vue.component('DetailColumn', DetailColumn)
45
43
 
@@ -47,3 +45,4 @@ Vue.component('AppBarButton', AppBarButton)
47
45
  Vue.component('AppBarTitle', AppBarTitle)
48
46
 
49
47
  Vue.component('Start', Start)
48
+ Vue.component('FlyingContext', FlyingContext)
@@ -28,7 +28,11 @@
28
28
  <a-table-row
29
29
  v-for="model in models_"
30
30
  :key="model.id"
31
- :class="getModelClass(model)"
31
+ v-flying-context-trigger="hasFlyingContext"
32
+ :class="{selectable: hasFlyingContext}"
33
+ v-bind="getRowAttributes(model)"
34
+ v-on="getRowListeners(model)"
35
+ @click="$emit('flyingContext', model)"
32
36
  >
33
37
  <v-icon
34
38
  v-if="$has.icon"
@@ -50,7 +54,6 @@
50
54
  <div
51
55
  v-for="model in models_"
52
56
  :key="model.id"
53
- :class="getModelClass(model)"
54
57
  >
55
58
  <slot
56
59
  name="model"
@@ -76,7 +79,7 @@ import { ListViewMixin } from '@a-vue/components/list/ListViewMixin'
76
79
  import { LoadingEvent } from '@a-vue/events'
77
80
 
78
81
  @Component({
79
- props: ['modelClass']
82
+ props: ['rowAttributes', 'rowListeners']
80
83
  })
81
84
  export default class ListView extends Mixins(ListViewMixin) {
82
85
  $hasOptions = ['icon']
@@ -93,8 +96,19 @@ export default class ListView extends Mixins(ListViewMixin) {
93
96
  this.$emit('update:isLoading', this.isLoading)
94
97
  }
95
98
 
96
- getModelClass (model) {
97
- return this.modelClass && this.modelClass(model)
99
+ getRowAttributes (model) {
100
+ if (typeof this.rowAttributes === 'function') {
101
+ return this.rowAttributes(model)
102
+ }
103
+ return this.rowAttributes
104
+ }
105
+
106
+ getRowListeners (model) {
107
+ return (this.rowListeners && this.rowListeners(model)) || {}
108
+ }
109
+
110
+ get hasFlyingContext () {
111
+ return !!this.$listeners.flyingContext
98
112
  }
99
113
 
100
114
  setFilter (name, value) {
@@ -1,151 +1,65 @@
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 #form="{modelToEdit, changed, valid}">
8
+ <slot
9
+ name="form"
10
+ :modelToEdit="modelToEdit"
11
+ :changed="changed"
12
+ :valid="valid"
13
+ />
14
+
15
+ <edit-form-buttons
16
+ :changed="changed"
17
+ :valid="valid"
18
+ :has="{reset: !!modelToEdit.id}"
19
+ @save="$emit('save', modelToEdit, ignoreChangesOnRouteChange)"
20
+ @reset="$refs.form.reset()"
21
+ />
22
+ </template>
23
+ </edit-form>
57
24
  </template>
58
25
 
59
26
  <script>
60
- import { Component, Mixins, Watch } from '@a-vue'
61
- import { EditPageMixin } from './EditPageMixin'
27
+ import { Component, Vue } from '@a-vue'
28
+ import { DialogEvent } from '@a-vue/events'
62
29
 
63
30
  @Component({
64
- props: [
65
- 'model',
66
- 'icon',
67
- 'title',
68
- 'listLink',
69
- 'getAction'
70
- ]
31
+ props: ['model', 'createModelToEdit']
71
32
  })
72
- export default class EditPage extends Mixins(EditPageMixin) {
73
- $hasOptions = ['detail', {list: false}]
74
-
75
- model_ = null
33
+ export default class EditPage extends Vue {
34
+ unregisterRouterHook = null
35
+ ignoreChangesOnRouteChange_ = false
76
36
 
77
37
  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
38
+ this.unregisterRouterHook = this.$router.beforeEach(async (to, from, next) => {
39
+ if (this.$refs.form.changed && !this.ignoreChangesOnRouteChange_) {
40
+ const result = await this.$events.dispatch(new DialogEvent(DialogEvent.SHOW, {
41
+ title: 'Änderungen verwerfen?',
42
+ message: 'Im Formular sind nicht gespeicherte Änderungen. Sollen diese verworfen werden?',
43
+ yesButton: 'Verwerfen'
44
+ }))
45
+ if (result === DialogEvent.RESULT_YES) {
46
+ next()
47
+ }
48
+ return
130
49
  }
131
- }
132
- return this.model.getLink('list')
133
- }
134
-
135
- createModelToEdit () {
136
- return this.model_.cloneForEdit(this.fields)
50
+ next()
51
+ })
137
52
  }
138
53
 
139
- get saveParams () {
140
- return {
141
- id: this.model.id
142
- }
54
+ destroyed () {
55
+ this.unregisterRouterHook()
143
56
  }
144
57
 
145
- afterSave (model) {
146
- this.model_ = model
147
- this.reset()
148
- this.$emit('saved', model)
58
+ /**
59
+ * hook to allow to leave a just created (saved) model
60
+ */
61
+ ignoreChangesOnRouteChange () {
62
+ this.ignoreChangesOnRouteChange_ = true
149
63
  }
150
64
  }
151
65
  </script>
@@ -0,0 +1,84 @@
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
+ let lastResult = null // cache last result because of hmr reload
11
+
12
+ function onLoad (callback) {
13
+ onLoadCallback = callback
14
+ }
15
+
16
+ function load (route) {
17
+ const Component = [...route.matched].pop().components.default
18
+
19
+ if (!Component.drm_getActions) {
20
+ console.warn('A data route component must implement a static drm_getActions() method.')
21
+ }
22
+
23
+ const request = Component.drm_getActions(route, onLoad)
24
+ if (Array.isArray(request)) {
25
+ return Promise.all(request.map(request => {
26
+ return request
27
+ .dispatchGlobalLoadingEvents()
28
+ .execute()
29
+ }))
30
+ } else {
31
+ return request
32
+ .dispatchGlobalLoadingEvents()
33
+ .execute()
34
+ }
35
+ }
36
+
37
+ @Component
38
+ export class DataRouteMixin extends Vue {
39
+ drm_isLoaded = false
40
+
41
+ async beforeRouteEnter (to, from, next) {
42
+ routerHookCalled = true
43
+ const result = await load(to)
44
+
45
+ next(vm => {
46
+ if (result !== false) {
47
+ vm.drm_onLoad(result)
48
+ vm.drm_isLoaded = true
49
+ }
50
+ routerHookCalled = false
51
+ })
52
+ }
53
+
54
+ /**
55
+ * triggered both, if route name or route params change
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
+
73
+ drm_onLoad (result) {
74
+ onLoadCallback(this, result)
75
+ lastResult = result
76
+ }
77
+
78
+ created () {
79
+ // hmr reload creates vm but not triggers route enter
80
+ if (!routerHookCalled) {
81
+ onLoadCallback(this, lastResult)
82
+ }
83
+ }
84
+ }
@@ -1,7 +1,3 @@
1
- import CreateRoute from '@a-admin/components/routes/CreateRoute'
2
- import DetailRoute from '@a-admin/components/routes/DetailRoute'
3
- import EditRoute from '@a-admin/components/routes/EditRoute'
4
- import ListRoute from '@a-admin/components/routes/ListRoute'
5
1
  import { routeConfigPlugin } from '@a-vue/plugins/route-config/RouteConfigPlugin'
6
2
 
7
3
  export default routeConfigPlugin
@@ -9,13 +5,6 @@ export default routeConfigPlugin
9
5
  linkActiveClass: 'active'
10
6
  })
11
7
 
12
- .defaultComponents({
13
- list: ListRoute,
14
- new: CreateRoute,
15
- detail: DetailRoute,
16
- edit: EditRoute
17
- })
18
-
19
8
  .defaultBreadcrumbTitles({
20
9
  edit: 'Bearbeiten',
21
10
  new: 'Neu'
@@ -0,0 +1,26 @@
1
+ import { Vue } from '@a-vue'
2
+
3
+ Vue.directive('flying-context-trigger', {
4
+ bind: function (el, bindings) {
5
+ if (bindings.value) {
6
+ el.$flyingContextObserver = new MutationObserver(() => {
7
+ if (!el.classList.contains('flyingContextTrigger')) {
8
+ el.classList.add('flyingContextTrigger')
9
+ }
10
+ })
11
+
12
+ el.$flyingContextObserver.observe(el, {
13
+ attributes: true,
14
+ attributeFilter: ['class']
15
+ })
16
+
17
+ el.classList.add('flyingContextTrigger')
18
+ }
19
+ },
20
+
21
+ unbind: function (el) {
22
+ if (el.$flyingContextObserver) {
23
+ el.$flyingContextObserver.disconnect()
24
+ }
25
+ }
26
+ })
@@ -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)