@afeefa/vue-app 0.0.62 → 0.0.65

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 (39) 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/components/ABreadcrumbs.vue +3 -2
  5. package/src/components/AModal.vue +21 -3
  6. package/src/components/ARichTextArea.vue +95 -85
  7. package/src/components/ATableRow.vue +4 -0
  8. package/src/components/flying-context/FlyingContextEvent.js +5 -0
  9. package/src/components/form/EditForm.vue +13 -3
  10. package/src/components/form/EditModal.vue +52 -35
  11. package/src/components/form/NestedEditForm.vue +4 -0
  12. package/src/components/form/fields/FormFieldRichTextArea.vue +5 -3
  13. package/src/components/list/ListViewMixin.js +14 -2
  14. package/src/components/search-select/SearchSelectList.vue +0 -1
  15. package/src/components/vue/Component.js +9 -2
  16. package/src/events.js +2 -0
  17. package/src-admin/bootstrap.js +1 -0
  18. package/src-admin/components/App.vue +55 -6
  19. package/src-admin/components/FlyingContext.vue +77 -0
  20. package/src-admin/components/FlyingContextContainer.vue +85 -0
  21. package/src-admin/components/StickyFooter.vue +42 -0
  22. package/src-admin/components/StickyFooterContainer.vue +66 -0
  23. package/src-admin/components/form/EditFormButtons.vue +8 -3
  24. package/src-admin/components/index.js +4 -7
  25. package/src-admin/components/list/ListView.vue +22 -9
  26. package/src-admin/components/pages/EditPage.vue +23 -9
  27. package/src-admin/components/routes/DataRouteMixin.js +24 -15
  28. package/src-admin/config/routing.js +0 -11
  29. package/src-admin/config/vuetify.js +7 -1
  30. package/src-admin/directives/index.js +26 -0
  31. package/src-admin/styles.scss +1 -4
  32. package/src-admin/components/pages/CreatePage.vue +0 -114
  33. package/src-admin/components/pages/DetailPage.vue +0 -143
  34. package/src-admin/components/pages/EditPageMixin.js +0 -96
  35. package/src-admin/components/pages/ListPage.vue +0 -55
  36. package/src-admin/components/routes/CreateRoute.vue +0 -15
  37. package/src-admin/components/routes/DetailRoute.vue +0 -85
  38. package/src-admin/components/routes/EditRoute.vue +0 -78
  39. package/src-admin/components/routes/ListRoute.vue +0 -110
@@ -1 +1 @@
1
- 0.0.62
1
+ 0.0.65
package/README.md CHANGED
@@ -1 +1,3 @@
1
- # @afeefa/vue-app
1
+ # @afeefa/vue-app
2
+
3
+ Force push :-);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afeefa/vue-app",
3
- "version": "0.0.62",
3
+ "version": "0.0.65",
4
4
  "description": "",
5
5
  "author": "Afeefa Kollektiv <kollektiv@afeefa.de>",
6
6
  "license": "MIT",
@@ -106,9 +106,10 @@ export default class ABreadcrumbs extends Vue {
106
106
  getItemTitle (title) {
107
107
  // title = title.concat(title).concat(title)
108
108
  if (title.length > 20) {
109
- title = title.slice(0, 10).trim() + '...' + title.slice(-10).trim()
109
+ // title = title.slice(0, 10).trim() + '...' + title.slice(-10).trim()
110
110
  }
111
- return title.toUpperCase()
111
+ return title
112
+ // return title.toUpperCase()
112
113
  }
113
114
  }
114
115
  </script>
@@ -7,6 +7,8 @@
7
7
  v-bind="$attrs"
8
8
  :contentClass="modalId"
9
9
  transition="v-fade-transition"
10
+ :persistent="true"
11
+ :no-click-animation="true"
10
12
  @click:outside="cancel"
11
13
  @keydown.esc="cancel"
12
14
  >
@@ -41,7 +43,7 @@ import { getZIndex } from 'vuetify/lib/util/helpers'
41
43
  import { ComponentWidthMixin } from './mixins/ComponentWidthMixin'
42
44
 
43
45
  @Component({
44
- props: ['show', 'title', 'triggerClass', 'anchorPosition']
46
+ props: ['show', 'title', 'beforeClose', 'triggerClass', 'anchorPosition']
45
47
  })
46
48
  export default class ADialog extends Mixins(UsesPositionServiceMixin, ComponentWidthMixin) {
47
49
  modalId = randomCssClass(10)
@@ -83,7 +85,15 @@ export default class ADialog extends Mixins(UsesPositionServiceMixin, ComponentW
83
85
  * and emit a visibility event
84
86
  */
85
87
  @Watch('show')
86
- showChanged () {
88
+ async showChanged () {
89
+ // check if a modal is allowed to be closed
90
+ if (this.modal && !this.show && this.beforeClose) {
91
+ const result = await this.beforeClose()
92
+ if (!result) {
93
+ return
94
+ }
95
+ }
96
+
87
97
  this.modal = this.show
88
98
  }
89
99
 
@@ -121,7 +131,15 @@ export default class ADialog extends Mixins(UsesPositionServiceMixin, ComponentW
121
131
  this.urp_registerPositionWatcher(this.position)
122
132
  }
123
133
 
124
- cancel () {
134
+ async cancel () {
135
+ // check if a modal is allowed to be closed
136
+ if (this.modal && this.beforeClose) {
137
+ const result = await this.beforeClose()
138
+ if (!result) {
139
+ return
140
+ }
141
+ }
142
+
125
143
  this.modal = false
126
144
  }
127
145
 
@@ -4,85 +4,84 @@
4
4
  v-if="editor"
5
5
  class="menu-bar"
6
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>
7
+ <div>
8
+ <v-btn
9
+ small
10
+ :class="['menu-button', {'is-active': focus && editor.isActive('bold')}]"
11
+ @click="editor.chain().focus().toggleBold().run()"
12
+ >
13
+ <v-icon>{{ boldIcon }}</v-icon>
14
+ </v-btn>
15
+
16
+ <v-btn
17
+ small
18
+ :class="['menu-button', {'is-active': focus && editor.isActive('italic')}]"
19
+ @click="editor.chain().focus().toggleItalic().run()"
20
+ >
21
+ <v-icon>{{ italicIcon }}</v-icon>
22
+ </v-btn>
23
+
24
+ <v-btn
25
+ small
26
+ :class="['menu-button', 'strike', {'is-active': focus && editor.isActive('strike')}]"
27
+ @click="editor.chain().focus().toggleStrike().run()"
28
+ >
29
+ <v-icon>{{ strikeIcon }}</v-icon>
30
+ </v-btn>
31
+
32
+ <v-btn
33
+ small
34
+ :class="['menu-button', {'is-active': focus && editor.isActive('heading', {level: 1})}]"
35
+ @click="editor.chain().focus().toggleHeading({level: 1}).run()"
36
+ >
37
+ <v-icon>{{ h1Icon }}</v-icon>
38
+ </v-btn>
39
+
40
+ <v-btn
41
+ small
42
+ :class="['menu-button', {'is-active': focus && editor.isActive('heading', {level: 2})}]"
43
+ @click="editor.chain().focus().toggleHeading({level: 2}).run()"
44
+ >
45
+ <v-icon>{{ h2Icon }}</v-icon>
46
+ </v-btn>
47
+
48
+ <v-btn
49
+ small
50
+ :class="['menu-button', {'is-active': focus && editor.isActive('bulletList')}]"
51
+ @click="editor.chain().focus().toggleBulletList().run()"
52
+ >
53
+ <v-icon>{{ ulIcon }}</v-icon>
54
+ </v-btn>
55
+
56
+ <v-btn
57
+ small
58
+ :class="['menu-button', {'is-active': focus && editor.isActive('orderedList')}]"
59
+ @click="editor.chain().focus().toggleOrderedList().run()"
60
+ >
61
+ <v-icon>{{ olIcon }}</v-icon>
62
+ </v-btn>
63
+
64
+ <v-btn
65
+ small
66
+ :class="['menu-button', {'is-active': focus && editor.isActive('blockquote')}]"
67
+ @click="editor.chain().focus().toggleBlockquote().run()"
68
+ >
69
+ <v-icon>{{ commentIcon }}</v-icon>
70
+ </v-btn>
71
+
72
+ <v-btn
73
+ small
74
+ class="menu-button"
75
+ :disabled="initialValue === editor.getHTML()"
76
+ @click="editor.chain().focus().undo().run()"
77
+ >
78
+ <v-icon>{{ undoIcon }}</v-icon>
79
+ </v-btn>
80
+ </div>
81
+
82
+ <div>
83
+ <slot name="buttons" />
84
+ </div>
86
85
  </div>
87
86
 
88
87
  <editor-content
@@ -119,6 +118,7 @@ import {
119
118
  export default class ARichTextArea extends Vue {
120
119
  editor = null
121
120
  internalValue = null
121
+ initialValue = null
122
122
  focus = false
123
123
 
124
124
  boldIcon = mdiFormatBold
@@ -133,6 +133,7 @@ export default class ARichTextArea extends Vue {
133
133
  redoIcon = mdiRotateRight
134
134
 
135
135
  created () {
136
+ this.initialValue = this.value
136
137
  this.internalValue = this.value
137
138
  }
138
139
 
@@ -158,14 +159,20 @@ export default class ARichTextArea extends Vue {
158
159
  this.$emit('blur')
159
160
  }
160
161
  })
161
-
162
- this.editor.commands.setContent(this.internalValue, false)
163
162
  }
164
163
 
165
164
  beforeDestroy () {
166
165
  this.editor.destroy()
167
166
  }
168
167
 
168
+ /**
169
+ * reset the text area to disable the undo button
170
+ * e.g. after saving the form while keeping it open
171
+ */
172
+ reset () {
173
+ this.initialValue = this.value
174
+ }
175
+
169
176
  @Watch('value')
170
177
  valueChanged () {
171
178
  this.internalValue = this.value
@@ -192,10 +199,6 @@ export default class ARichTextArea extends Vue {
192
199
 
193
200
 
194
201
  <style lang="scss" scoped>
195
- .v-input:not(.v-input--is-focused) ::v-deep .v-counter {
196
- display: none;
197
- }
198
-
199
202
  .a-rich-text-editor {
200
203
  ::v-deep .ProseMirror {
201
204
  &-focused {
@@ -205,9 +208,12 @@ export default class ARichTextArea extends Vue {
205
208
  }
206
209
 
207
210
  .menu-bar {
211
+ display: flex;
212
+ justify-content: space-between;
208
213
  margin: -.2rem 0 .5rem -.2rem;
209
214
  }
210
215
 
216
+ .menu-bar .v-btn,
211
217
  .menu-button {
212
218
  padding: 0 !important;
213
219
  width: 30px !important;
@@ -239,6 +245,10 @@ export default class ARichTextArea extends Vue {
239
245
  &.is-active {
240
246
  background: #ECECEC !important;
241
247
  }
248
+
249
+ &[disabled] {
250
+ background: none !important;
251
+ }
242
252
  }
243
253
 
244
254
  ::v-deep .ProseMirror {
@@ -62,6 +62,10 @@ export default class ATableRow extends Vue {
62
62
  }
63
63
  }
64
64
 
65
+ &.selectable {
66
+ cursor: pointer;
67
+ }
68
+
65
69
  &:hover, &.selected {
66
70
  background: #F4F4F4;
67
71
  }
@@ -0,0 +1,5 @@
1
+ import { BaseEvent } from '@a-vue/plugins/event-bus/BaseEvent'
2
+
3
+ export class FlyingContextEvent extends BaseEvent {
4
+ static HIDE_ALL = 'FlyingContextEvent:hide-all'
5
+ }
@@ -4,9 +4,10 @@
4
4
  autocomplete="off"
5
5
  >
6
6
  <slot
7
+ name="form"
7
8
  :changed="changed"
8
9
  :valid="valid"
9
- :model="modelToEdit"
10
+ :modelToEdit="modelToEdit"
10
11
  />
11
12
  </v-form>
12
13
  </template>
@@ -27,16 +28,22 @@ export default class EditForm extends Vue {
27
28
  modelToEdit = null
28
29
  valid = false
29
30
  lastJson = null
31
+ forcedUnchange = false
30
32
 
31
33
  created () {
32
34
  this.reset()
33
35
  }
34
36
 
37
+ forceUnchanged () {
38
+ this.forcedUnchange = true
39
+ this.$emit('update:changed', false)
40
+ }
41
+
35
42
  reset () {
36
43
  if (this.createModelToEdit) {
37
44
  this.modelToEdit = this.createModelToEdit(this.model)
38
- } else {
39
- this.modelToEdit = this.model
45
+ } else if (this.model) {
46
+ this.modelToEdit = this.model.cloneForEdit()
40
47
  }
41
48
  this.lastJson = this.json
42
49
  }
@@ -65,6 +72,9 @@ export default class EditForm extends Vue {
65
72
  }
66
73
 
67
74
  get changed () {
75
+ if (this.forcedUnchange) {
76
+ return false
77
+ }
68
78
  // console.log(this.json)
69
79
  // console.log(this.lastJson)
70
80
  return this.json !== this.lastJson
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <a-modal
3
3
  :title="title"
4
+ :beforeClose="beforeClose"
4
5
  :show.sync="show_"
5
6
  v-bind="$attrs"
6
7
  >
@@ -10,16 +11,18 @@
10
11
 
11
12
  <edit-form
12
13
  v-if="show_"
14
+ ref="form"
13
15
  :model="model"
16
+ :createModelToEdit="createModelToEdit"
14
17
  >
15
- <template #fields>
18
+ <template #form="{modelToEdit, changed, valid}">
16
19
  <slot
17
- name="fields"
18
- :model="model"
20
+ name="form"
21
+ :modelToEdit="modelToEdit"
22
+ :changed="changed"
23
+ :valid="valid"
19
24
  />
20
- </template>
21
25
 
22
- <template #default="{changed, valid}">
23
26
  <a-row
24
27
  class="mt-8 mb-1 pb-1 gap-4"
25
28
  right
@@ -31,25 +34,14 @@
31
34
  Schließen
32
35
  </v-btn>
33
36
 
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>
43
-
44
- <v-icon
45
- v-if="changed"
46
- small
47
- text
48
- @click="reset"
49
- >
50
- {{ undoIcon }}
51
- </v-icon>
52
- </a-row>
37
+ <edit-form-buttons
38
+ :changed="changed"
39
+ :valid="valid"
40
+ small
41
+ :has="{reset: !!modelToEdit.id}"
42
+ @save="$emit('save', modelToEdit, ignoreChangesOnClose)"
43
+ @reset="$refs.form.reset()"
44
+ />
53
45
  </a-row>
54
46
  </template>
55
47
  </edit-form>
@@ -59,21 +51,26 @@
59
51
 
60
52
  <script>
61
53
  import { Component, Vue, Watch } from '@a-vue'
62
- import { mdiRotateLeft} from '@mdi/js'
54
+ import { DialogEvent } from '@a-vue/events'
63
55
 
64
56
  @Component({
65
- props: ['model', 'title', 'show']
57
+ props: ['model', 'createModelToEdit', 'title', 'show']
66
58
  })
67
59
  export default class EditModal extends Vue {
68
60
  show_ = false
61
+ ignoreChangesOnClose_ = false
69
62
 
70
- undoIcon = mdiRotateLeft
63
+ created () {
64
+ if (this.show) { // open on create with v-show
65
+ this.open()
66
+ }
67
+ }
71
68
 
72
69
  /**
73
70
  * visiblility changes from outside
74
71
  * this will trigger the show_ watcher,
75
72
  * forward the change to the modal and
76
- * later emit a open/close event
73
+ * later emit an open/close event
77
74
  */
78
75
  @Watch('show')
79
76
  showChanged () {
@@ -90,7 +87,6 @@ export default class EditModal extends Vue {
90
87
  @Watch('show_')
91
88
  show_Changed () {
92
89
  if (this.show_) {
93
- this.reset()
94
90
  this.$emit('open')
95
91
  } else {
96
92
  this.$emit('close')
@@ -101,16 +97,37 @@ export default class EditModal extends Vue {
101
97
  this.show_ = true
102
98
  }
103
99
 
104
- close () {
105
- this.show_ = false
100
+ async beforeClose () {
101
+ // run only if show_ is true to prevent double checks with a-modal
102
+ if (this.show_ && this.$refs.form.changed && !this.ignoreChangesOnClose_) {
103
+ const result = await this.$events.dispatch(new DialogEvent(DialogEvent.SHOW, {
104
+ title: 'Änderungen verwerfen?',
105
+ message: 'Im Formular sind nicht gespeicherte Änderungen. Sollen diese verworfen werden?',
106
+ yesButton: 'Verwerfen'
107
+ }))
108
+ if (result !== DialogEvent.RESULT_YES) {
109
+ return false
110
+ }
111
+ }
112
+ return true
106
113
  }
107
114
 
108
- reset () {
109
- this.$emit('reset')
115
+ async close () {
116
+ const result = await this.beforeClose()
117
+ if (!result) {
118
+ return
119
+ }
120
+
121
+ this.show_ = false
110
122
  }
111
123
 
112
- save () {
113
- this.$emit('save')
124
+ /**
125
+ * hook to allow to leave a just created (saved) model
126
+ */
127
+ ignoreChangesOnClose () {
128
+ // this.$refs.form.forceUnchanged()
129
+ console.info('TODO switch form to forceUnchanged')
130
+ this.ignoreChangesOnClose_ = true
114
131
  }
115
132
  }
116
133
  </script>
@@ -21,5 +21,9 @@ export default class NestedEditForm extends Vue {
21
21
  console.warn('Nested edit form does not have a model.')
22
22
  }
23
23
  }
24
+
25
+ get modelToEdit () {
26
+ return this.model
27
+ }
24
28
  }
25
29
  </script>
@@ -1,7 +1,9 @@
1
1
  <template>
2
- <a-rich-text-area
3
- v-model="model[name]"
4
- />
2
+ <a-rich-text-area v-model="model[name]">
3
+ <template #buttons>
4
+ <slot name="buttons" />
5
+ </template>
6
+ </a-rich-text-area>
5
7
  </template>
6
8
 
7
9
  <script>
@@ -64,9 +64,16 @@ export class ListViewMixin extends Vue {
64
64
  .on('change', this.filtersChanged) // listen to change
65
65
 
66
66
  this._filtersInitialized()
67
+ this.$emit('update:filters', this.filters)
68
+ this.$emit('update:listViewModel', this.listViewModel)
67
69
 
68
70
  if (this.models) {
69
71
  this.$emit('update:count', this.meta_.count_search)
72
+
73
+ this.$emit('onLoad', {
74
+ models: this.models_,
75
+ meta: this.meta_
76
+ })
70
77
  } else {
71
78
  this.load()
72
79
  }
@@ -132,7 +139,7 @@ export class ListViewMixin extends Vue {
132
139
 
133
140
  if (!models) { // error happened
134
141
  this.isLoading = false
135
- this.$emit('update:isLoading', this.isLoading)
142
+ this.$emit('update:isLoading', false)
136
143
  return
137
144
  }
138
145
 
@@ -144,8 +151,13 @@ export class ListViewMixin extends Vue {
144
151
  }
145
152
 
146
153
  this.isLoading = false
147
- this.$emit('update:isLoading', this.isLoading)
154
+ this.$emit('update:isLoading', false)
148
155
 
149
156
  this.$emit('update:count', this.meta_.count_search)
157
+
158
+ this.$emit('onLoad', {
159
+ models,
160
+ meta
161
+ })
150
162
  }
151
163
  }
@@ -67,7 +67,6 @@ export default class SearchSelectList extends Mixins(ListViewMixin) {
67
67
  if (this.q) {
68
68
  this.filters.q.value = this.q
69
69
  }
70
- this.$emit('update:filters', this.filters)
71
70
  }
72
71
  }
73
72
  </script>
@@ -32,9 +32,16 @@ function propsWithDefaults (props) {
32
32
  // property: { some object }, should be a normal vue props object
33
33
  } else if (value && typeof value === 'object' && value.constructor === Object) {
34
34
  normalizedProps[subProp] = value
35
- // property: true, null, ...
35
+ // property: true, false, null, ...
36
36
  } else {
37
- normalizedProps[subProp] = { default: value }
37
+ if (typeof value === 'boolean') {
38
+ normalizedProps[subProp] = {
39
+ type: Boolean,
40
+ default: value
41
+ }
42
+ } else {
43
+ normalizedProps[subProp] = { default: value }
44
+ }
38
45
  }
39
46
  })
40
47
  } else {
package/src/events.js CHANGED
@@ -1,4 +1,6 @@
1
+ export { BaseEvent } from './plugins/event-bus/BaseEvent'
1
2
  export { LoadingEvent } from './components/loading-indicator/LoadingEvent'
2
3
  export { SaveEvent } from './components/save-indicator/SaveEvent'
3
4
  export { AlertEvent } from './components/alert/AlertEvent'
4
5
  export { DialogEvent } from './components/dialog/DialogEvent'
6
+ export { FlyingContextEvent } from './components/flying-context/FlyingContextEvent'
@@ -1,5 +1,6 @@
1
1
  import './config/event-bus'
2
2
  import './config/components'
3
+ import './directives'
3
4
 
4
5
  import { translationPlugin } from '@a-admin/plugins/translation/TranslationPlugin'
5
6
  import { apiResourcesPlugin } from '@a-vue/plugins/api-resources/ApiResourcesPlugin'