@oxygen-cms/ui 1.7.2 → 1.8.0

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 (50) hide show
  1. package/{.eslintrc.js → .eslintrc.json} +3 -3
  2. package/package.json +14 -4
  3. package/src/CrudApi.js +23 -9
  4. package/src/MediaApi.js +3 -3
  5. package/src/PagesApi.js +48 -0
  6. package/src/PartialsApi.js +30 -0
  7. package/src/components/AuthenticatedLayout.vue +3 -1
  8. package/src/components/CodeEditor.vue +1 -1
  9. package/src/components/GroupsChooser.vue +2 -2
  10. package/src/components/GroupsList.vue +2 -2
  11. package/src/components/PageActions.vue +28 -0
  12. package/src/components/PageEdit.vue +164 -0
  13. package/src/components/PageNestedPagination.vue +27 -0
  14. package/src/components/PageNestedRow.vue +52 -0
  15. package/src/components/PageStatusIcon.vue +33 -0
  16. package/src/components/PageTable.vue +156 -0
  17. package/src/components/PartialActions.vue +28 -0
  18. package/src/components/PartialList.vue +74 -0
  19. package/src/components/PartialStatusIcon.vue +29 -0
  20. package/src/components/PartialTable.vue +65 -0
  21. package/src/components/ResourceList.vue +132 -0
  22. package/src/components/UserManagement.vue +2 -2
  23. package/src/components/UserProfileForm.vue +1 -1
  24. package/src/components/UserProfilePage.vue +1 -1
  25. package/src/components/content/CommandsList.vue +108 -0
  26. package/src/components/content/ContentEditor.vue +489 -0
  27. package/src/components/content/GridCellNodeView.vue +82 -0
  28. package/src/components/content/GridRowNodeView.vue +53 -0
  29. package/src/components/content/HtmlNodeView.vue +89 -0
  30. package/src/components/content/MarkMenu.vue +116 -0
  31. package/src/components/content/MediaNodeView.vue +83 -0
  32. package/src/components/content/ObjectLinkNodeView.vue +181 -0
  33. package/src/components/content/PartialNodeView.vue +217 -0
  34. package/src/components/content/commands.js +72 -0
  35. package/src/components/content/suggestion.js +211 -0
  36. package/src/components/media/MediaChooseDirectory.vue +2 -2
  37. package/src/components/media/MediaDirectory.vue +1 -1
  38. package/src/components/media/MediaInsertModal.vue +11 -2
  39. package/src/components/media/MediaItem.vue +1 -1
  40. package/src/components/media/MediaItemPreview.vue +18 -2
  41. package/src/components/media/MediaList.vue +4 -5
  42. package/src/components/media/MediaUpload.vue +1 -1
  43. package/src/components/media/media.scss +1 -0
  44. package/src/components/pages/PageList.vue +65 -0
  45. package/src/components/users/CreateUserModal.vue +1 -1
  46. package/src/components/util.css +1 -1
  47. package/src/icons.js +33 -5
  48. package/src/main.js +4 -0
  49. package/src/modules/PagesPartials.js +74 -2
  50. package/src/styles/pages-table.scss +34 -0
@@ -0,0 +1,89 @@
1
+ <template>
2
+ <NodeViewWrapper>
3
+ <div :class="{wrapper: true, editable: isEditable, selected: isEditable && selected}">
4
+ <template v-if="editing && isEditable">
5
+ <CodeEditor lang="html" height="20em" :value="node.attrs.code" @input="onInput"></CodeEditor>
6
+ <b-field class="toolbar">
7
+ <p class="control"><b-button size="is-small" icon-left="save" @click="save"></b-button></p>
8
+ <p class="control"><b-button size="is-small" icon-left="trash" @click="removeSelf"></b-button></p>
9
+ </b-field>
10
+ </template>
11
+ <template v-else>
12
+ <b-field v-if="isEditable" class="toolbar">
13
+ <p class="control"><b-button size="is-small" data-drag-handle icon-left="grip-vertical"></b-button></p>
14
+ <p class="control"><b-button size="is-small" icon-left="pencil-alt" @click="editing = true"></b-button></p>
15
+ <p class="control"><b-button size="is-small" icon-left="trash" @click="removeSelf"></b-button></p>
16
+ </b-field>
17
+ <div v-html="node.attrs.code"></div>
18
+ </template>
19
+ </div>
20
+ </NodeViewWrapper>
21
+ </template>
22
+
23
+ <script>
24
+ import { nodeViewProps, NodeViewWrapper } from '@tiptap/vue-2'
25
+ import CodeEditor from "../CodeEditor.vue";
26
+ export default {
27
+ name: "HtmlNodeView",
28
+ components: {CodeEditor, NodeViewWrapper},
29
+ props: nodeViewProps,
30
+ data() {
31
+ return {
32
+ editing: false,
33
+ latestValue: null
34
+ }
35
+ },
36
+ computed: {
37
+ isEditable() {
38
+ return this.editor && this.editor.isEditable;
39
+ }
40
+ },
41
+ watch: {
42
+ 'node': 'syncLatestValue'
43
+ },
44
+ mounted() {
45
+ if(!this.node.attrs.code) {
46
+ this.editing = true;
47
+ }
48
+ this.syncLatestValue();
49
+ },
50
+ methods: {
51
+ onInput(value) {
52
+ this.latestValue = value;
53
+ },
54
+ save() {
55
+ console.log("save", this.latestValue);
56
+ this.updateAttributes({
57
+ code: this.latestValue
58
+ });
59
+ this.editing = false;
60
+ },
61
+ syncLatestValue() {
62
+ this.latestValue = this.node.attrs.code;
63
+ },
64
+ removeSelf() {
65
+ this.deleteNode();
66
+ }
67
+ }
68
+ }
69
+ </script>
70
+
71
+ <style scoped>
72
+ .wrapper {
73
+ position: relative;
74
+ }
75
+ .wrapper.editable {
76
+ min-height: 4rem;
77
+ outline: 1px dashed green;
78
+ }
79
+ .wrapper.selected {
80
+ outline: 2px solid green;
81
+ }
82
+
83
+ .toolbar {
84
+ z-index: 9;
85
+ position: absolute;
86
+ top: 0.5rem;
87
+ right: 0.5rem;
88
+ }
89
+ </style>
@@ -0,0 +1,116 @@
1
+ <template>
2
+ <bubble-menu ref="bubbleMenu" :editor="editor" :should-show="shouldShowMarksMenu">
3
+ <div class="bubble-menu">
4
+ <p class="control"><b-button size="is-small" :type="editor.isActive('bold') ? 'is-info' : 'is-light'" icon-left="bold" @click="toggleMark('bold')"></b-button></p>
5
+ <p class="control"><b-button size="is-small" :type="editor.isActive('italic') ? 'is-info' : 'is-light'" icon-left="italic" @click="toggleMark('italic')"></b-button></p>
6
+ <p class="control"><b-button size="is-small" :type="editor.isActive('underline') ? 'is-info' : 'is-light'" icon-left="underline" @click="toggleMark('underline')"></b-button></p>
7
+ <p class="control"><b-button size="is-small" :type="editor.isActive('strike') ? 'is-info' : 'is-light'" icon-left="strikethrough" @click="toggleMark('strike')"></b-button></p>
8
+ <p class="control"><b-button size="is-small" :type="linkPanelActive ? 'is-info' : 'is-light'" icon-left="link" @click="toggleMark('link')"></b-button>
9
+ </p>
10
+ <p class="control"><b-button size="is-small" type="is-light" icon-left="remove-format" @click="editor.chain().focus().unsetAllMarks().run()"></b-button></p>
11
+ </div>
12
+ <div v-show="linkPanelActive" type="is-light">
13
+ <div class="modal-card" style="z-index: 10000;">
14
+ <header class="modal-card-head">
15
+ <p class="modal-card-title">Edit Link</p>
16
+ <b-button icon-left="external-link-alt" @click="openLink">Open link</b-button>
17
+ </header>
18
+ <div class="modal-card-body">
19
+ <b-field label="URL" label-position="on-border">
20
+ <b-input :value="linkUrl" @input="v => onLinkUpdate({linkUrl: v, linkOpenInNewWindow: linkOpenInNewWindow})"></b-input>
21
+ </b-field>
22
+ <b-switch :value="linkOpenInNewWindow" @input="v => onLinkUpdate({linkOpenInNewWindow: v, linkUrl: linkUrl})">Open in new window</b-switch>
23
+ <br>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </bubble-menu>
28
+ </template>
29
+
30
+ <script>
31
+ import {debounce} from "lodash";
32
+ import {BubbleMenu, isTextSelection} from "@tiptap/vue-2";
33
+
34
+ export default {
35
+ name: "MarkMenu",
36
+ components: {BubbleMenu},
37
+ props: {
38
+ editor: {type: Object, required: true}
39
+ },
40
+ data() {
41
+ return {
42
+ linkPanelActive: false,
43
+ linkUrl: '',
44
+ linkOpenInNewWindow: false
45
+ }
46
+ },
47
+ methods: {
48
+ openLink() {
49
+ window.open(this.linkUrl, '_blank').focus();
50
+ },
51
+ onLinkUpdate: debounce(function ({ linkUrl, linkOpenInNewWindow }) {
52
+ this.linkUrl = linkUrl;
53
+ this.linkOpenInNewWindow = linkOpenInNewWindow;
54
+ let props = { href: linkUrl };
55
+ if(linkOpenInNewWindow) {
56
+ props.target = '_blank';
57
+ }
58
+ console.log('setLink', props);
59
+ let oldSelection = this.editor.state.selection.getBookmark();
60
+ this.editor.chain().extendMarkRange('link').setLink(props).run();
61
+ this.editor.view.dispatch(this.editor.state.tr.setSelection(oldSelection.resolve(this.editor.state.doc)));
62
+ }, 1000),
63
+ toggleMark(ty) {
64
+ let oldSelection = this.editor.state.selection.getBookmark();
65
+ let cmd = this.editor.chain().focus()
66
+ if(this.editor.isActive(ty))
67
+ {
68
+ cmd = cmd.extendMarkRange(ty);
69
+ }
70
+ let uppercaseTy = ty.charAt(0).toUpperCase() + ty.slice(1);;
71
+ let toggle = cmd['toggle' + uppercaseTy];
72
+ toggle().run();
73
+ this.editor.view.dispatch(this.editor.state.tr.setSelection(oldSelection.resolve(this.editor.state.doc)));
74
+ },
75
+ shouldShowMarksMenu({view, state, from, to}) {
76
+ const { doc, selection } = state
77
+ const { empty } = selection
78
+
79
+ let isNodeSelection = selection.node;
80
+
81
+ // Sometime check for `empty` is not enough.
82
+ // Doubleclick an empty paragraph returns a node size of 2.
83
+ // So we check also for an empty text size.
84
+ const isEmptyTextBlock = !doc.textBetween(from, to).length
85
+ && isTextSelection(state.selection)
86
+
87
+ // When clicking on an element inside the bubble menu the editor "blur" event
88
+ // is called and the bubble menu item is focussed. In this case we should
89
+ // consider the menu as part of the editor and keep showing the menu
90
+ const isChildOfMenu = this.$refs.bubbleMenu.$el.contains(document.activeElement)
91
+
92
+ const hasEditorFocus = view.hasFocus() || isChildOfMenu
93
+
94
+ const isMarkActive = this.editor.isActive('link') || this.editor.isActive('bold') || this.editor.isActive('italic') || this.editor.isActive('underline') || this.editor.isActive('strike');
95
+
96
+ if(this.editor.isActive('link'))
97
+ {
98
+ let attrs = this.editor.getAttributes('link');
99
+ this.linkUrl = attrs.href;
100
+ this.linkOpenInNewWindow = attrs.target === '_blank';
101
+ this.linkPanelActive = true;
102
+ } else {
103
+ this.linkPanelActive = false;
104
+ }
105
+
106
+ return hasEditorFocus && ((!empty && !isEmptyTextBlock && !isNodeSelection) || isMarkActive) && this.editor.isEditable;
107
+ },
108
+ }
109
+ }
110
+ </script>
111
+
112
+ <style scoped>
113
+ .bubble-menu {
114
+ display: flex;
115
+ }
116
+ </style>
@@ -0,0 +1,83 @@
1
+ <template>
2
+ <NodeViewWrapper :class="{selected: isEditable && selected}">
3
+ <MediaInsertModal :multiselect-allowed="false" :active="mediaSelectActive" @select="selectItem" @close="removeSelf"></MediaInsertModal>
4
+ <MediaItemPreview v-if="item" :item="item">
5
+ <b-field v-if="isEditable && selected">
6
+ <p class="control">
7
+ <b-button icon-left="grip-vertical" size="is-small" data-drag-handle></b-button>
8
+ </p>
9
+ <p class="control">
10
+ <b-button size="is-small" icon-left="file-image" @click="mediaSelectActive=true"></b-button>
11
+ </p>
12
+ <p class="control">
13
+ <b-button size="is-small" icon-left="trash" @click="removeSelf"></b-button>
14
+ </p>
15
+ </b-field>
16
+ </MediaItemPreview>
17
+ <em v-else>No media item selected</em>
18
+ </NodeViewWrapper>
19
+ </template>
20
+
21
+ <script>
22
+ import { nodeViewProps, NodeViewWrapper } from '@tiptap/vue-2'
23
+ import MediaInsertModal from "../media/MediaInsertModal.vue";
24
+ import MediaItemPreview from "../media/MediaItemPreview.vue";
25
+ import MediaApi from "../../MediaApi.js";
26
+
27
+ export default {
28
+ name: "MediaNodeView",
29
+ components: {MediaItemPreview, MediaInsertModal, NodeViewWrapper },
30
+ props: nodeViewProps,
31
+ data() {
32
+ return {
33
+ mediaApi: new MediaApi(),
34
+ mediaSelectActive: false,
35
+ item: null
36
+ }
37
+ },
38
+ computed: {
39
+ isEditable() {
40
+ return this.editor.isEditable;
41
+ }
42
+ },
43
+ watch: {
44
+ 'node.attrs.id': 'fetchMediaItem'
45
+ },
46
+ mounted() {
47
+ if(!this.node.attrs.id) {
48
+ this.mediaSelectActive = true;
49
+ }
50
+ this.fetchMediaItem()
51
+ },
52
+ methods: {
53
+ async fetchMediaItem() {
54
+ if(!this.node.attrs.id) {
55
+ this.item = null;
56
+ return;
57
+ }
58
+ if(this.item !== null && this.item.id === this.node.attrs.id) { return; }
59
+ console.log('fetching media item');
60
+ this.item = (await this.mediaApi.get(this.node.attrs.id)).item;
61
+ },
62
+ selectItem(items) {
63
+ console.assert(items.length === 1);
64
+ this.updateAttributes({
65
+ id: items[0].id
66
+ });
67
+ this.mediaSelectActive = false;
68
+ this.editor.chain().focus().run();
69
+ },
70
+ removeSelf() {
71
+ this.deleteNode()
72
+ this.editor.chain().focus().insertContent({type: 'paragraph', text: ''}).run();
73
+ }
74
+ }
75
+ }
76
+ </script>
77
+
78
+ <style scoped lang="scss">
79
+ @import "../../styles/_variables.scss";
80
+ .selected {
81
+ outline: 2px solid $danger;
82
+ }
83
+ </style>
@@ -0,0 +1,181 @@
1
+ <template>
2
+ <NodeViewWrapper class="object-link">
3
+ <b-skeleton v-if="!resolvedHref"></b-skeleton>
4
+ <a v-else-if="!isEditable" :href="getApiHost() + resolvedHref">{{ node.attrs.content ? node.attrs.content : defaultTitle }}</a>
5
+ <a v-else href="#" @click="onLinkClicked">{{ node.attrs.content ? node.attrs.content : defaultTitle }}</a>
6
+ <b-modal :active="node.attrs.type === 'page' && !node.attrs.id" has-modal-card aria-modal width="80%" class="choose-page" @close="deleteNode">
7
+ <div class="modal-card">
8
+ <div class="modal-card-head">
9
+ <p class="modal-card-title">Choose a page</p>
10
+ </div>
11
+ <div class="modal-card-body is-flex">
12
+ <PageList>
13
+ <template #actions="props">
14
+ <b-button type="is-primary" @click="selectedPage(props.row)">Select</b-button>
15
+ </template>
16
+ </PageList>
17
+ </div>
18
+ </div>
19
+ </b-modal>
20
+ <MediaInsertModal :active="node.attrs.type === 'media' && !node.attrs.id" :multiselect-allowed="false" close-verb="Remove link" :action-verb="shouldReturnToEditModal ? 'Choose' : 'Insert link'" @close="deleteNode" @select="selectedMedia"></MediaInsertModal>
21
+ <b-modal :active="editLinkModalActive" has-modal-card aria-modal @update:active="v => editLinkModalActive = v">
22
+ <div class="modal-card">
23
+ <div class="modal-card-head is-flex">
24
+ <div>
25
+ <p class="modal-card-title">Edit Link</p>
26
+ </div>
27
+ <div class="is-flex-grow-1"></div>
28
+ <b-button icon-left="trash" @click="deleteNode">Remove</b-button>
29
+ </div>
30
+ <div class="modal-card-body">
31
+ <b-field label="Title">
32
+ <b-input :value="node.attrs.content" :placeholder="defaultTitle" @input="v => updateAttributes({ content: v })"></b-input>
33
+ </b-field>
34
+ <div class="label">Target</div>
35
+ <div class="card">
36
+ <div class="card-content">
37
+ <div v-if="node.attrs.type === 'media' && resolvedObject" class="media is-align-items-center">
38
+ <div class="media-content">
39
+ <p class="title is-4">{{ resolvedObject.name }}</p>
40
+ <p class="subtitle is-6">{{ resolvedObject.fullPath }}</p>
41
+ </div>
42
+ <div class="media-right buttons">
43
+ <b-button icon-left="images" @click="clearId(true)">Change...</b-button>
44
+ </div>
45
+ </div>
46
+ <div v-if="node.attrs.type === 'page' && resolvedObject" class="media">
47
+ <div class="media-icon">
48
+ <b-icon icon="file-alt" size="is-large" class="mr-4 huge-icon"></b-icon>
49
+ </div>
50
+ <div class="media-content">
51
+ <p class="title is-4">{{ resolvedObject.title }}</p>
52
+ <p class="subtitle is-6">{{ resolvedObject.slug }}</p>
53
+ </div>
54
+ <div class="media-right buttons">
55
+ <b-button icon-left="images" @click="clearId(true)">Change...</b-button>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <div v-if="node.attrs.type === 'media' && resolvedObject" class="card-image">
60
+ <MediaItemPreview :item="resolvedObject" />
61
+ </div>
62
+ </div>
63
+ <!-- <b-button @click="clearId">Select different object...</b-button>-->
64
+ </div>
65
+ </div>
66
+ </b-modal>
67
+ </NodeViewWrapper>
68
+ </template>
69
+
70
+ <script>
71
+ import { nodeViewProps, NodeViewWrapper } from '@tiptap/vue-2'
72
+ import {getApiRoot} from "../../CrudApi.js";
73
+ import {FetchBuilder, getApiHost} from "../../api.js";
74
+ import MediaInsertModal from "../media/MediaInsertModal.vue";
75
+ import MediaItemPreview from "../media/MediaItemPreview.vue";
76
+ import {getDirectoryPathString} from "../../MediaDirectoryApi.js";
77
+ import PageList from "../pages/PageList.vue";
78
+
79
+ export default {
80
+ name: "ObjectLinkNodeView",
81
+ components: {MediaInsertModal, NodeViewWrapper, MediaItemPreview, PageList },
82
+ props: nodeViewProps,
83
+ data() {
84
+ return {
85
+ getDirectoryPathString: getDirectoryPathString,
86
+ editLinkModalActive: false,
87
+ shouldReturnToEditModal: false,
88
+ resolvedHref: null,
89
+ resolvedObject: null,
90
+ defaultTitle: '',
91
+ getApiHost
92
+ }
93
+ },
94
+ computed: {
95
+ isEditable() {
96
+ return this.editor.isEditable;
97
+ }
98
+ },
99
+ watch: {
100
+ 'node.attrs.id': 'resolveUrl',
101
+ 'node.attrs.type': 'resolveUrl'
102
+ },
103
+ mounted() {
104
+ this.resolveUrl()
105
+ },
106
+ methods: {
107
+ async resolveUrl() {
108
+ if(!this.node.attrs.id) {
109
+ this.resolvedHref = null;
110
+ return;
111
+ }
112
+ let result = await this.resolveLink({ type: this.node.attrs.type, id: this.node.attrs.id });
113
+ console.log(result);
114
+ // trim the leading '/' from the URL
115
+ this.resolvedHref = result.url.substring(1);
116
+ this.defaultTitle = result.title;
117
+ this.resolvedObject = result.object;
118
+ },
119
+ async resolveLink(params) {
120
+ return await (FetchBuilder.default(this.$buefy, 'get').withQueryParams(params).fetch(getApiRoot() + 'object-link/resolve'));
121
+ },
122
+ async selectedItem(info) {
123
+ let resolved = await this.resolveLink(info);
124
+ if(this.shouldReturnToEditModal) {
125
+ this.editLinkModalActive = true;
126
+ }
127
+ this.updateAttributes({
128
+ ...info,
129
+ content: resolved.title
130
+ });
131
+ },
132
+ async selectedPage(page) {
133
+ return await this.selectedItem({ type: 'page', id: page.id });
134
+ },
135
+ async selectedMedia(items) {
136
+ if(items.length !== 1) {
137
+ throw new Error('expected exactly 1 item');
138
+ }
139
+ return await this.selectedItem({ type: 'media', id: items[0].id});
140
+ },
141
+ clearId(shouldReturnToEditModal) {
142
+ this.shouldReturnToEditModal = shouldReturnToEditModal;
143
+ this.editLinkModalActive = false;
144
+ this.updateAttributes({
145
+ id: null
146
+ });
147
+ },
148
+ onLinkClicked() {
149
+ this.editLinkModalActive = true
150
+ }
151
+ }
152
+ }
153
+ </script>
154
+
155
+ <style scoped>
156
+ .object-link {
157
+ display: inline;
158
+ }
159
+
160
+ .media-icon ::v-deep .huge-icon {
161
+ width: 6rem;
162
+ height: 6rem;
163
+ font-size: 300%;
164
+ }
165
+
166
+ .choose-page ::v-deep .modal-card {
167
+ width: 100%;
168
+ height: 100%;
169
+ max-width: 1600px;
170
+ }
171
+
172
+ .choose-page ::v-deep .animation-content {
173
+ max-height: calc(100vh - 10rem);
174
+ height: 100%;
175
+ }
176
+
177
+ ::v-deep .b-skeleton {
178
+ width: 10rem;
179
+ margin: 0 0.5rem;
180
+ }
181
+ </style>