@live-change/content-frontend 0.2.15 → 0.2.17

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.
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="absolute top-0 right-0 pr-4 max-h-0 flex align-items-center z-5">
3
- <router-link :to="{ name: 'content:pageEditor', params: { pageId: page } }" class="no-underline">
3
+ <router-link :to="editorRoute(objectType, object)" class="no-underline">
4
4
  <Button icon="pi pi-pencil"
5
5
  class="p-button p-button-icon-only p-button-rounded p-button-warning mr-2" />
6
6
  </router-link>
@@ -14,13 +14,25 @@
14
14
  import Button from 'primevue/button'
15
15
 
16
16
  const props = defineProps({
17
- page: {
17
+ objectType: {
18
+ type: String,
19
+ required: true
20
+ },
21
+ object: {
18
22
  type: String,
19
23
  required: true
20
24
  },
21
25
  name: {
22
26
  type: String,
23
27
  default: ""
28
+ },
29
+ editorRoute: {
30
+ type: Function,
31
+ default: (objectType, object) => {
32
+ const [service, type] = objectType.split('_')
33
+ const prop = type[0].toLowerCase()+type.slice(1)
34
+ return { name: `${service}:${prop}Editor`, params: { [prop+'Id']: object }}
35
+ }
24
36
  }
25
37
  })
26
38
 
@@ -1,9 +1,143 @@
1
1
  <template>
2
2
 
3
+ <DocumentEditor :targetType="objectType" :target="object"
4
+ :purpose="purpose" :type="contentType"
5
+ :config="contentConfig || defaultContentConfig"
6
+ :class="className" :style="style"
7
+ v-model:saveState="saveState" v-model:version="version">
8
+
9
+ <template #menuEnd="{}">
10
+
11
+ <router-link :to="previewRoute(objectType, object)" target="_blank"
12
+ class="no-underline">
13
+ <Button icon="pi pi-eye" label="Preview" class="p-button-secondary p-button-sm mr-1 mb-1" />
14
+ </router-link>
15
+
16
+ <div class="p-buttonset mr-1 mb-1 border-round">
17
+ <Button type="button"
18
+ class="p-button p-component p-button-sm p-button-outlined p-button-secondary cursor-auto inline-block"
19
+ :class="{ 'p-disabled': saveState == 'saving' }">
20
+ <span class="pi p-button-icon p-button-icon-left"
21
+ :class="[saveState == 'saving' ? 'pi-sync' : 'pi-hashtag' ]" />
22
+ <span class="p-button-label">{{ ( version ?? 0 ).toFixed().padStart(10, '0') }}</span>
23
+ </Button>
24
+ <Button icon="pi pi-save" label="Publish" class="p-button-success p-button-sm" type="button"
25
+ :disabled="saveState == 'saving'" @click="publish" />
26
+ </div>
27
+
28
+ </template>
29
+
30
+ </DocumentEditor>
31
+
32
+ <div></div><!-- Because bug in vue -->
33
+
3
34
  </template>
4
35
 
5
36
  <script setup>
6
37
 
38
+ import Button from 'primevue/button'
39
+
40
+ import { DocumentEditor, EditorMenu } from "@live-change/wysiwyg-frontend"
41
+
42
+ import defaultContentConfig from "./contentConfig.js"
43
+
44
+ const props = defineProps({
45
+ objectType: {
46
+ type: String,
47
+ required: true
48
+ },
49
+ object: {
50
+ type: String,
51
+ required: true
52
+ },
53
+ previewRoute: {
54
+ type: Function,
55
+ default: (objectType, object) => {
56
+ const [service, type] = objectType.split('_')
57
+ const prop = type[0].toLowerCase()+type.slice(1)
58
+ return { name: `${service}:${prop}Preview`, params: { [prop]: object }}
59
+ }
60
+ },
61
+ publishTarget: {
62
+ type: String,
63
+ required: true
64
+ },
65
+ contentType: {
66
+ type: String,
67
+ required: true
68
+ },
69
+ purpose: {
70
+ type: String,
71
+ required: true
72
+ },
73
+ contentConfig: {
74
+ type: Object,
75
+ default: () => null
76
+ },
77
+ class: {},
78
+ style: {}
79
+ })
80
+ import { toRefs } from "@vueuse/core"
81
+ const {
82
+ objectType, object, previewRoute, publishTarget, contentType, purpose, contentConfig,
83
+ class: className, style
84
+ } = toRefs(props)
85
+
86
+ import { computed, watch, ref, onMounted, inject } from 'vue'
87
+
88
+ const document = computed(() => `${JSON.stringify(objectType.value)}:${JSON.stringify(object.value)}`)
89
+
90
+ const saveState = ref()
91
+ const version = ref()
92
+
93
+ import { useToast } from 'primevue/usetoast'
94
+ const toast = useToast()
95
+ import { useConfirm } from 'primevue/useconfirm'
96
+ const confirm = useConfirm()
97
+
98
+ const workingZone = inject('workingZone')
99
+
100
+
101
+ import { useApi, path, live } from '@live-change/vue3-ssr'
102
+
103
+ const api = useApi()
104
+
105
+ const p = path()
106
+
107
+ const liveCanonicalUrlPath = computed(
108
+ () => p.url.targetOwnedCanonical({ targetType: objectType.value, target: object.value })
109
+ )
110
+
111
+ const [canonicalUrlData] = await Promise.all([
112
+ live(liveCanonicalUrlPath),
113
+ ]).catch(e => [null, null, null, null])
114
+
115
+
116
+ function publish() {
117
+ const snapshotVersion = version.value
118
+ const typeName = objectType.value.split('_')[1]
119
+ const typeNameLower = typeName[0].toLowerCase()+typeName.slice(1)
120
+ confirm.require({
121
+ target: event.currentTarget,
122
+ message: `Do you want to publish this content version ${snapshotVersion} to ${publishTarget.value}`,
123
+ icon: 'pi pi-info-circle',
124
+ acceptClass: 'p-button-danger',
125
+ accept: async () => {
126
+ api.actions.content.publish({
127
+ objectType: objectType.value, object: object.value, version: snapshotVersion, type: contentType.value
128
+ }).then(() => {
129
+ toast.add({ severity: 'success', summary: 'Published', detail: typeName+' published', life: 3000 })
130
+ }).catch(e => {
131
+ toast.add({ severity: 'error', summary: 'Error', detail: 'Error publishing ' + typeNameLower, life: 3000 })
132
+ })
133
+ },
134
+ reject: () => {
135
+ toast.add({ severity:'error', summary: 'Rejected', detail: 'You have rejected', life: 3000 })
136
+ }
137
+ })
138
+
139
+ }
140
+
7
141
  </script>
8
142
 
9
143
  <style scoped>
@@ -0,0 +1,71 @@
1
+ <template>
2
+
3
+ <LimitedAccess :requiredRoles="['writer']" :objectType="objectType" :object="object" hidden>
4
+ <div class="absolute top-0 right-0 pr-4 max-h-0 flex align-items-center z-5">
5
+ <Badge severity="warning" value="This is page preview" class="mr-2" />
6
+ <router-link :to="editorRoute(objectType, object)" class="no-underline">
7
+ <Button icon="pi pi-pencil"
8
+ class="p-button p-button-icon-only p-button-rounded p-button-warning mr-2" />
9
+ </router-link>
10
+ </div>
11
+ <Content :objectType="objectType" :object="object" :style="style" :class="clazz" preview />
12
+ </LimitedAccess>
13
+
14
+ </template>
15
+
16
+ <script setup>
17
+ import Button from "primevue/button"
18
+ import Badge from "primevue/badge"
19
+
20
+ import { ResolveUrl, NotFound } from "@live-change/url-frontend"
21
+ import { LimitedAccess } from "@live-change/access-control-frontend";
22
+ import Content from "./Content.vue"
23
+
24
+ import { computed, watch, ref, onMounted } from 'vue'
25
+ import { toRefs } from "@vueuse/core"
26
+ import { useHost } from "@live-change/frontend-base"
27
+
28
+ const isMounted = ref(false)
29
+ onMounted(() => isMounted.value = true)
30
+
31
+ import { path, live, useApi } from '@live-change/vue3-ssr'
32
+ const api = useApi()
33
+ const p = path()
34
+
35
+ const urlMore = [
36
+ url => p.content.page({ page: url.target })
37
+ ]
38
+
39
+ const props = defineProps({
40
+ objectType: {
41
+ type: String,
42
+ required: true
43
+ },
44
+ object: {
45
+ type: String,
46
+ required: true
47
+ },
48
+ class: {
49
+ type: String,
50
+ default: ''
51
+ },
52
+ style: {
53
+ type: String,
54
+ default: ''
55
+ },
56
+ editorRoute: {
57
+ type: Function,
58
+ default: (objectType, object) => {
59
+ const [service, type] = objectType.split('_')
60
+ const prop = type[0].toLowerCase()+type.slice(1)
61
+ return { name: `${service}:${prop}Editor`, params: { [prop+'Id']: object }}
62
+ }
63
+ },
64
+ })
65
+ const { objectType, object, class: clazz, style } = toRefs(props)
66
+
67
+ </script>
68
+
69
+ <style scoped>
70
+
71
+ </style>
@@ -0,0 +1,80 @@
1
+ <template>
2
+ <Accordion :multiple="true" class="w-full mb-2">
3
+ <AccordionTab>
4
+ <template #header>
5
+ <UrlsInfo :targetType="objectType" :target="object" class="w-full" />
6
+ </template>
7
+ <Urls :key="key" :targetType="objectType" :target="object" />
8
+ </AccordionTab>
9
+ <AccordionTab>
10
+ <template #header>
11
+ <span class="font-bold mr-1">Public access: {{ publicAccessLevel }}</span>
12
+ </template>
13
+ <AccessControl :objectType="objectType" :object="object" />
14
+ </AccordionTab>
15
+ <AccordionTab>
16
+ <template #header>
17
+ <span v-if="metadata" class="font-bold mr-1">Metadata:</span>
18
+ <span v-if="metadata" class="mr-1 font-normal">{{ metadata.title }}</span>
19
+ <span v-else class="font-bold text-red-600">Metadata not set</span>
20
+ </template>
21
+ <MetadataEditor :objectType="objectType" :object="object" :key="key"></MetadataEditor>
22
+ </AccordionTab>
23
+ </Accordion>
24
+ </template>
25
+
26
+ <script setup>
27
+
28
+ import Accordion from 'primevue/accordion'
29
+ import AccordionTab from 'primevue/accordiontab'
30
+ import Button from 'primevue/button'
31
+
32
+ import { UrlsInfo, Urls, NotFound } from '@live-change/url-frontend'
33
+ import { AccessControl } from '@live-change/access-control-frontend'
34
+ import MetadataEditor from "./MetadataEditor.vue"
35
+
36
+ const props = defineProps({
37
+ objectType: {
38
+ type: String,
39
+ required: true
40
+ },
41
+ object: {
42
+ type: String,
43
+ required: true
44
+ }
45
+ })
46
+
47
+ import { toRefs } from "@vueuse/core"
48
+ import { computed } from 'vue'
49
+
50
+ const { objectType, object } = toRefs(props)
51
+
52
+ const key = computed(() => `${objectType.value}:${object.value}`)
53
+
54
+ import { useApi, path, live } from '@live-change/vue3-ssr'
55
+ const api = useApi()
56
+ const p = path()
57
+
58
+ const livePublicAccessPath = computed(
59
+ () => p.accessControl.objectOwnedPublicAccess({ objectType: objectType.value, object: object.value })
60
+ )
61
+ const liveMetadataPath = computed(
62
+ () => p.content.objectOwnedMetadata({ objectType: objectType.value, object: object.value })
63
+ )
64
+
65
+ const [publicAccessData, metadata] = await Promise.all([
66
+ live(livePublicAccessPath),
67
+ live(liveMetadataPath)
68
+ ]).catch(e => [null, null, null, null])
69
+
70
+ const publicAccessLevel = computed(() => {
71
+ if(publicAccessData?.sessionRoles?.includes('reader')) return 'session'
72
+ if(publicAccessData?.userRoles?.includes('reader')) return 'user'
73
+ return 'none'
74
+ })
75
+
76
+ </script>
77
+
78
+ <style scoped>
79
+
80
+ </style>
@@ -11,10 +11,6 @@
11
11
  object: {
12
12
  type: String,
13
13
  required: true
14
- },
15
- url: {
16
- type: Object,
17
- required: true
18
14
  }
19
15
  })
20
16
 
@@ -58,75 +54,77 @@
58
54
  const canonicalUrl = `https://${canonicalUrlDomain}/${canonical.value?.path ?? ''}`
59
55
 
60
56
  let ogImage = []
61
- if(m.ogImage) {
62
- const image = m.ogImage
63
- ogImage = [
64
- { property: 'og:image', content: `https://${canonicalUrlDomain}/api/image/image/${image.id}` },
65
- { property: 'og:image:width', content: image.width },
66
- { property: 'og:image:height', content: image.height },
67
- { property: 'og:image:type', content: 'image/'+image.extension }
68
- ]
69
- }
57
+ if(m) {
58
+ if (m.ogImage) {
59
+ const image = m.ogImage
60
+ ogImage = [
61
+ {property: 'og:image', content: `https://${canonicalUrlDomain}/api/image/image/${image.id}`},
62
+ {property: 'og:image:width', content: image.width},
63
+ {property: 'og:image:height', content: image.height},
64
+ {property: 'og:image:type', content: 'image/' + image.extension}
65
+ ]
66
+ }
70
67
 
71
- useMeta({
72
- title: m.title,
73
- description: m.description,
74
- link: [
75
- { rel: 'canonical', href: canonicalUrl }
76
- ],
77
- meta: [
78
- metaProperty('og:title', m.og.title),
79
- metaProperty('og:description', m.og.description),
80
- ...ogImage,
81
- metaProperty('og:determiner', m.og.determiner),
82
- metaProperty('og:locale', m.og.locale),
83
- ...metaProperties('og:locale:alternate', m.og.localeAlternate),
84
- metaProperty('og:type', m.og.type),
85
-
86
- metaProperty('og:music:duration', m.og.music.duration),
87
- ...metaPropertiesObjects('og:music:album', m.og.music.album),
88
- ...metaPropertiesObjects('og:music:song', m.og.music.song),
89
- ...metaProperties('og:music:musician', m.og.music.duration),
90
- metaProperty('og:music:release_date', m.og.music.releaseDate),
91
- ...metaProperties('og:music:creator', m.og.music.creator),
92
-
93
- metaProperty('og:video:duration', m.og.video.duration),
94
- metaProperty('og:viceo:release_date', m.og.video.releaseDate),
95
- ...metaPropertiesObjects('og:video:duration', m.og.video.actors),
96
- ...metaProperties('og:video:director', m.og.video.director),
97
- ...metaProperties('og:video:writer', m.og.video.writer),
98
- ...metaProperties('og:video:series', m.og.video.series),
99
- ...metaProperties('og:video:tag', m.og.video.tag),
100
-
101
- ...metaProperties('og:profile:first_name', m.og.profile.firstName),
102
- ...metaProperties('og:profile:last_name', m.og.profile.lastName),
103
- ...metaProperties('og:profile:username', m.og.profile.username),
104
- ...metaProperties('og:profile:gender', m.og.profile.gender),
105
-
106
- metaProperty('og:article:published_time', m.og.article.publishedTime),
107
- metaProperty('og:article:modified_time', m.og.article.modifiedTime),
108
- metaProperty('og:article:expiration_time', m.og.article.expirationTime),
109
- ...metaProperties('og:article:author', m.og.article.author),
110
- metaProperty('og:article:section', m.og.article.section),
111
- ...metaProperties('og:article:tag', m.og.article.tag),
112
-
113
- ...metaProperties('og:book:author', m.og.book.author),
114
- metaProperty('og:book:isbn', m.og.book.isbn),
115
- metaProperty('og:book:release_date', m.og.book.releaseDate),
116
- ...metaProperties('og:book:tag', m.og.book.tag),
117
-
118
- { property: 'og:url', content: canonicalUrl },
119
-
120
-
121
- /* { property: 'og:url', content: m.url },
122
- { property: 'og:type', content: 'website' },
123
- { property: 'twitter:card', content: 'summary_large_image' },
124
- { property: 'twitter:title', content: metadata.value.title },
125
- { property: 'twitter:description', content: metadata.value.description },
126
- { property: 'twitter:image', content: metadata.value.image },
127
- { property: 'twitter:url', content: metadata.value.url }*/
128
- ].filter(x => !!x)
129
- })
68
+ useMeta({
69
+ title: m.title,
70
+ description: m.description,
71
+ link: [
72
+ {rel: 'canonical', href: canonicalUrl}
73
+ ],
74
+ meta: [
75
+ metaProperty('og:title', m.og.title),
76
+ metaProperty('og:description', m.og.description),
77
+ ...ogImage,
78
+ metaProperty('og:determiner', m.og.determiner),
79
+ metaProperty('og:locale', m.og.locale),
80
+ ...metaProperties('og:locale:alternate', m.og.localeAlternate),
81
+ metaProperty('og:type', m.og.type),
82
+
83
+ metaProperty('og:music:duration', m.og.music.duration),
84
+ ...metaPropertiesObjects('og:music:album', m.og.music.album),
85
+ ...metaPropertiesObjects('og:music:song', m.og.music.song),
86
+ ...metaProperties('og:music:musician', m.og.music.duration),
87
+ metaProperty('og:music:release_date', m.og.music.releaseDate),
88
+ ...metaProperties('og:music:creator', m.og.music.creator),
89
+
90
+ metaProperty('og:video:duration', m.og.video.duration),
91
+ metaProperty('og:viceo:release_date', m.og.video.releaseDate),
92
+ ...metaPropertiesObjects('og:video:duration', m.og.video.actors),
93
+ ...metaProperties('og:video:director', m.og.video.director),
94
+ ...metaProperties('og:video:writer', m.og.video.writer),
95
+ ...metaProperties('og:video:series', m.og.video.series),
96
+ ...metaProperties('og:video:tag', m.og.video.tag),
97
+
98
+ ...metaProperties('og:profile:first_name', m.og.profile.firstName),
99
+ ...metaProperties('og:profile:last_name', m.og.profile.lastName),
100
+ ...metaProperties('og:profile:username', m.og.profile.username),
101
+ ...metaProperties('og:profile:gender', m.og.profile.gender),
102
+
103
+ metaProperty('og:article:published_time', m.og.article.publishedTime),
104
+ metaProperty('og:article:modified_time', m.og.article.modifiedTime),
105
+ metaProperty('og:article:expiration_time', m.og.article.expirationTime),
106
+ ...metaProperties('og:article:author', m.og.article.author),
107
+ metaProperty('og:article:section', m.og.article.section),
108
+ ...metaProperties('og:article:tag', m.og.article.tag),
109
+
110
+ ...metaProperties('og:book:author', m.og.book.author),
111
+ metaProperty('og:book:isbn', m.og.book.isbn),
112
+ metaProperty('og:book:release_date', m.og.book.releaseDate),
113
+ ...metaProperties('og:book:tag', m.og.book.tag),
114
+
115
+ {property: 'og:url', content: canonicalUrl},
116
+
117
+
118
+ /* { property: 'og:url', content: m.url },
119
+ { property: 'og:type', content: 'website' },
120
+ { property: 'twitter:card', content: 'summary_large_image' },
121
+ { property: 'twitter:title', content: metadata.value.title },
122
+ { property: 'twitter:description', content: metadata.value.description },
123
+ { property: 'twitter:image', content: metadata.value.image },
124
+ { property: 'twitter:url', content: metadata.value.url }*/
125
+ ].filter(x => !!x)
126
+ })
127
+ }
130
128
  </script>
131
129
 
132
130
  <style scoped>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <div class="absolute top-0 right-0 pr-4 max-h-0 flex align-items-center z-5">
3
+ <router-link :to="editorRoute(objectType, object)" class="no-underline">
4
+ <Button icon="pi pi-pencil"
5
+ class="p-button p-button-icon-only p-button-rounded p-button-warning mr-2" />
6
+ </router-link>
7
+ </div>
8
+ </template>
9
+
10
+ <script setup>
11
+ import Button from 'primevue/button'
12
+
13
+ const props = defineProps({
14
+ path: {
15
+ type: String,
16
+ required: true
17
+ },
18
+ objectType: {
19
+ type: String,
20
+ required: true
21
+ },
22
+ object: {
23
+ type: String,
24
+ required: true
25
+ },
26
+ editorRoute: {
27
+ type: Function,
28
+ default: (objectType, object) => {
29
+ const [service, type] = objectType.split('_')
30
+ const prop = type[0].toLowerCase()+type.slice(1)
31
+ return { name: `${service}:${prop}Editor`, params: { [prop+'Id']: object }}
32
+ }
33
+ }
34
+ })
35
+
36
+ import { toRefs } from "@vueuse/core"
37
+
38
+ const { objectType, object, editorRoute } = toRefs(props)
39
+
40
+ </script>
41
+
42
+ <style scoped>
43
+
44
+ </style>
@@ -2,7 +2,7 @@
2
2
  <div class="absolute top-0 right-0 pr-4 max-h-0 flex align-items-center z-5">
3
3
  <Button icon="pi pi-plus"
4
4
  class="p-button p-button-icon-only p-button-rounded p-button-danger"
5
- @click="createPage" />
5
+ @click="createContent" />
6
6
  </div>
7
7
  </template>
8
8
 
@@ -13,15 +13,36 @@
13
13
  path: {
14
14
  type: String,
15
15
  required: true
16
+ },
17
+ objectType: {
18
+ type: String,
19
+ required: true
20
+ },
21
+ editorRoute: {
22
+ type: Function,
23
+ default: (objectType, object) => {
24
+ const [service, type] = objectType.split('_')
25
+ const prop = type[0].toLowerCase()+type.slice(1)
26
+ return { name: `${service}:${prop}Editor`, params: { [prop+'Id']: object }}
27
+ }
28
+ },
29
+ createActionName: {
30
+ type: String,
31
+ default: null
32
+ },
33
+ createActionParams: {
34
+ type: Object,
35
+ default: () => {}
16
36
  }
17
37
  })
18
38
 
19
39
  import { toRefs } from "@vueuse/core"
40
+ import { computed } from "vue"
20
41
 
21
42
  import { useHost } from "@live-change/frontend-base"
22
43
  const host = useHost()
23
44
 
24
- const { path } = toRefs(props)
45
+ const { path, objectType } = toRefs(props)
25
46
 
26
47
  import { useToast } from 'primevue/usetoast'
27
48
  const toast = useToast()
@@ -31,20 +52,32 @@
31
52
  import { useRouter } from 'vue-router'
32
53
  const router = useRouter()
33
54
 
34
- function createPage() {
55
+ const serviceName = computed(() => {
56
+ const [service, type] = objectType.value.split('_')
57
+ return service
58
+ })
59
+ const typeName = computed(() => {
60
+ const [service, type] = objectType.value.split('_')
61
+ return type
62
+ })
63
+ const typeNameLower = computed(() => typeName.value[0].toLowerCase()+typeName.value.slice(1))
64
+
65
+ function createContent() {
35
66
  confirm.require({
36
67
  target: event.currentTarget,
37
- message: `Do you want to create page at ${host}/${path.value} ?`,
68
+ message: `Do you want to create ${typeNameLower.value} at ${host}/${path.value} ?`,
38
69
  icon: 'pi pi-info-circle',
39
70
  acceptClass: 'p-button-danger',
40
71
  accept: async () => {
41
- const pageId = await api.actions.content.createPage()
72
+ const contentId = await api.actions[serviceName.value][props.createActionName || `create${typeName.value}`]({
73
+ ...props.createActionParams
74
+ })
42
75
  await api.actions.url.takeUrl({
43
- target: pageId, targetType: 'content_Page', path: path.value, domain: host, redirect: false
76
+ target: contentId, targetType: objectType.value, path: path.value, domain: host, redirect: false
44
77
  })
45
- toast.add({ severity:'success', summary: 'Page created', life: 1500 })
78
+ toast.add({ severity:'success', summary: `${typeName} created`, life: 1500 })
46
79
  setTimeout(() => {
47
- router.push({ name: 'content:pageEditor', params: { pageId } })
80
+ router.push(props.editorRoute(objectType.value, contentId))
48
81
  }, 200)
49
82
  },
50
83
  reject: () => {
@@ -1,61 +1,19 @@
1
1
  <template>
2
- <ResolveUrl targetType="content_Page" :path="urlPath" :fetchMore="urlMore">
3
- <template #default="{ target, style, class: clazz }">
4
- <Metadata objectType="content_Page" :object="target" />
5
- <LimitedAccess :requiredRoles="['writer']" objectType="content_Page" :object="target" hidden>
6
- <PageAdminButtons :page="target" :style="style" :class="clazz" :name="urlPath.value" />
7
- </LimitedAccess>
8
- <Content objectType="content_Page" :object="target" />
9
- </template>
10
- <template #notFound="{ path, style, class: clazz }">
11
- <NotFoundAdminButtons v-if="canCreatePage" :path="urlPath" :style="style" :class="clazz" />
12
- <NotFound :style="style" :class="clazz" />
13
- </template>
14
- </ResolveUrl>
2
+ <UrlContent objectType="content_Page" :path="urlPath" :class="clazz" :style="style" />
15
3
  </template>
16
4
 
17
5
  <script setup>
18
- import Button from "primevue/button"
19
- import PageAdminButtons from "./PageAdminButtons.vue"
20
- import NotFoundAdminButtons from "./NotFoundAdminButtons.vue"
21
6
 
22
- import { ResolveUrl, NotFound } from "@live-change/url-frontend"
23
- import { LimitedAccess } from "@live-change/access-control-frontend";
24
- import Content from "./Content.vue"
25
- import Metadata from "./Metadata.vue"
26
-
27
- import { computed, watch, ref, onMounted } from 'vue'
28
7
  import { toRefs } from "@vueuse/core"
29
-
30
- const isMounted = ref(false)
31
- onMounted(() => isMounted.value = true)
32
-
33
- import { path, live, useApi } from '@live-change/vue3-ssr'
34
- const api = useApi()
35
- const p = path()
36
-
37
- const urlMore = [
38
- url => p.content.page({ page: url.target }),
39
- url => p.content.content({ objectType: 'content_Page', object: url.target }),
40
- url => p.content.objectOwnedMetadata({ objectType: 'content_Page', object: url.target }),
41
- url => p.url.targetOwnedCanonical({ targetType: 'content_Page', target: url.target })
42
- ]
43
-
44
- const canCreatePage = computed(() => api.client.value.roles.includes('writer'))
8
+ import UrlContent from "./UrlContent.vue";
45
9
 
46
10
  const props = defineProps({
47
11
  path: {
48
12
  type: String,
49
13
  required: true
50
14
  },
51
- class: {
52
- type: String,
53
- default: ''
54
- },
55
- style: {
56
- type: String,
57
- default: ''
58
- }
15
+ class: { },
16
+ style: { }
59
17
  })
60
18
  const { path: urlPath, class: clazz, style } = toRefs(props)
61
19
 
@@ -1,62 +1,19 @@
1
1
  <template>
2
- <Accordion v-if="pageData" :multiple="true" class="w-full mb-2">
3
- <AccordionTab>
4
- <template #header>
5
- <UrlsInfo targetType="content_Page" :target="pageId" class="w-full" />
6
- </template>
7
- <Urls :key="pageId" targetType="content_Page" :target="pageId" />
8
- </AccordionTab>
9
- <AccordionTab>
10
- <template #header>
11
- <span class="font-bold mr-1">Public access: {{ publicAccessLevel }}</span>
12
- </template>
13
- <AccessControl objectType="content_Page" :object="pageId" />
14
- </AccordionTab>
15
- <AccordionTab>
16
- <template #header>
17
- <span v-if="metadata" class="font-bold mr-1">Metadata:</span>
18
- <span v-if="metadata" class="mr-1 font-normal">{{ metadata.title }}</span>
19
- <span v-else class="font-bold text-red-600">Metadata not set</span>
20
- </template>
21
- <MetadataEditor objectType="content_Page" :object="pageId" :key="pageId"></MetadataEditor>
22
- </AccordionTab>
23
- </Accordion>
24
-
25
- <DocumentEditor v-if="pageData" targetType="content_Page" :target="pageId" purpose="page"
26
- :config="contentConfig" type="page" v-model:saveState="saveState" v-model:version="version">
27
- <template #menuEnd="{}">
28
-
29
- <Button icon="pi pi-eye" label="Preview" class="p-button-secondary p-button-sm mr-1 mb-1" />
30
-
31
- <div class="p-buttonset mr-1 mb-1 border-round">
32
- <button type="button"
33
- class="p-button p-component p-button-sm p-button-outlined p-button-secondary cursor-auto inline-block"
34
- :class="{ 'p-disabled': saveState == 'saving' }">
35
- <span class="pi p-button-icon p-button-icon-left"
36
- :class="[saveState == 'saving' ? 'pi-sync' : 'pi-hashtag' ]" />
37
- <span class="p-button-label">{{ ( version ?? 0 ).toFixed().padStart(10, '0') }}</span>
38
- </button>
39
- <Button icon="pi pi-save" label="Publish" class="p-button-success p-button-sm" type="button"
40
- :disabled="saveState == 'saving'" @click="publish" />
41
- </div>
42
-
43
- </template>
44
- </DocumentEditor>
2
+ <!-- <ContentSettings v-if="pageData" objectType="content_Page" :object="pageId" />-->
45
3
 
4
+ <ContentEditor v-if="pageData"
5
+ objectType="content_Page" :object="pageId"
6
+ :publishTarget="`page `+`${canonicalUrlData.domain ?? '*'}/${canonicalUrlData.path}`"
7
+ contentType="page" purpose="page" />
46
8
  <NotFound v-if="pageData === null" />
9
+
47
10
  </template>
48
11
 
49
12
  <script setup>
13
+ import { NotFound } from "@live-change/url-frontend";
50
14
 
51
- import Accordion from 'primevue/accordion'
52
- import AccordionTab from 'primevue/accordiontab'
53
- import Button from 'primevue/button'
54
-
55
- import { UrlsInfo, Urls, NotFound } from '@live-change/url-frontend'
56
- import { DocumentEditor, EditorMenu } from "@live-change/wysiwyg-frontend"
57
- import { AccessControl } from '@live-change/access-control-frontend'
58
- import MetadataEditor from "./MetadataEditor.vue"
59
- import contentConfig from "./contentConfig.js";
15
+ import ContentSettings from "./ContentSettings.vue"
16
+ import ContentEditor from "./ContentEditor.vue"
60
17
 
61
18
  import { computed, watch, ref, onMounted, inject } from 'vue'
62
19
  import { toRefs } from "@vueuse/core"
@@ -74,22 +31,8 @@
74
31
  })
75
32
  const { pageId, class: clazz, style } = toRefs(props)
76
33
 
77
- const document = computed(() => `${JSON.stringify('content_Page')}:${JSON.stringify(pageId.value)}`)
78
-
79
- const saveState = ref()
80
- const version = ref()
81
-
82
- import { useToast } from 'primevue/usetoast'
83
- const toast = useToast()
84
- import { useConfirm } from 'primevue/useconfirm'
85
- const confirm = useConfirm()
86
-
87
- const workingZone = inject('workingZone')
88
-
89
34
  import { useApi, path, live } from '@live-change/vue3-ssr'
90
-
91
35
  const api = useApi()
92
-
93
36
  const p = path()
94
37
 
95
38
  const livePagePath = computed(
@@ -112,36 +55,11 @@
112
55
  live(liveMetadataPath)
113
56
  ]).catch(e => [null, null, null, null])
114
57
 
115
- const publicAccessLevel = computed(() => {
116
- if(publicAccessData?.sessionRoles?.includes('reader')) return 'session'
117
- if(publicAccessData?.userRoles?.includes('reader')) return 'user'
118
- return 'none'
119
- })
120
58
 
121
- function publish() {
122
- const snapshotVersion = version.value
123
-
124
- confirm.require({
125
- target: event.currentTarget,
126
- message: `Do you want to publish this content version ${snapshotVersion} to page `+
127
- `${canonicalUrlData.value.domain ?? '*'}/${canonicalUrlData.value.path}?`,
128
- icon: 'pi pi-info-circle',
129
- acceptClass: 'p-button-danger',
130
- accept: async () => {
131
- api.actions.content.publish({
132
- objectType: 'content_Page', object: pageId.value, version: snapshotVersion, type: 'content'
133
- }).then(() => {
134
- toast.add({ severity: 'success', summary: 'Published', detail: 'Page published', life: 3000 })
135
- }).catch(e => {
136
- toast.add({ severity: 'error', summary: 'Error', detail: 'Error publishing page', life: 3000 })
137
- })
138
- },
139
- reject: () => {
140
- toast.add({ severity:'error', summary: 'Rejected', detail: 'You have rejected', life: 3000 })
141
- }
142
- })
143
-
144
- }
59
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
60
+
61
+ console.log("WAITING!")
62
+ await sleep(5000)
145
63
 
146
64
  </script>
147
65
 
@@ -1,40 +1,11 @@
1
1
  <template>
2
-
3
- <LimitedAccess :requiredRoles="['writer']" objectType="content_Page" :object="pageId" hidden>
4
- <div class="absolute top-0 right-0 pr-4 max-h-0 flex align-items-center z-5">
5
- <Badge severity="warning" value="This is page preview" class="mr-2" />
6
- <router-link :to="{ name: 'content:pageEditor', params: { pageId } }" class="no-underline">
7
- <Button icon="pi pi-pencil"
8
- class="p-button p-button-icon-only p-button-rounded p-button-warning mr-2" />
9
- </router-link>
10
- </div>
11
- <Content objectType="content_Page" :object="pageId" :style="style" :class="clazz" preview />
12
- </LimitedAccess>
13
-
2
+ <ContentPreview objectType="content_Page" :object="pageId" :style="style" :class="clazz" />
14
3
  </template>
15
4
 
16
5
  <script setup>
17
- import Button from "primevue/button"
18
- import Badge from "primevue/badge"
19
6
 
20
- import { ResolveUrl, NotFound } from "@live-change/url-frontend"
21
- import { LimitedAccess } from "@live-change/access-control-frontend";
22
- import Content from "./Content.vue"
23
-
24
- import { computed, watch, ref, onMounted } from 'vue'
7
+ import ContentPreview from "./ContentPreview.vue";
25
8
  import { toRefs } from "@vueuse/core"
26
- import { useHost } from "@live-change/frontend-base"
27
-
28
- const isMounted = ref(false)
29
- onMounted(() => isMounted.value = true)
30
-
31
- import { path, live, useApi } from '@live-change/vue3-ssr'
32
- const api = useApi()
33
- const p = path()
34
-
35
- const urlMore = [
36
- url => p.content.page({ page: url.target })
37
- ]
38
9
 
39
10
  const props = defineProps({
40
11
  pageId: {
@@ -51,7 +22,6 @@
51
22
  }
52
23
  })
53
24
  const { pageId, class: clazz, style } = toRefs(props)
54
-
55
25
  </script>
56
26
 
57
27
  <style scoped>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <ResolveUrl :targetType="objectType" :path="urlPath" :fetchMore="urlMore">
3
+ <template #default="{ target, style, class: clazz }">
4
+ <Metadata :objectType="objectType" :object="target" />
5
+ <LimitedAccess :requiredRoles="['writer']" :objectType="objectType" :object="target" hidden>
6
+ <ContentAdminButtons :objectType="objectType" :object="target" :style="style" :class="clazz"
7
+ :name="urlPath.value" :editorRoute="editorRoute" />
8
+ </LimitedAccess>
9
+ <Content :objectType="objectType" :object="target" class="w-full" :style="style" :class="clazz" />
10
+ </template>
11
+ <template #notFound="{ path, style, class: clazz }">
12
+ <NotFoundAdminButtons v-if="canCreateContent" :path="urlPath"
13
+ :objectType="objectType" :editorRoute="editorRoute"
14
+ :style="style" :class="clazz" />
15
+ <NotFound :style="style" :class="clazz" />
16
+ </template>
17
+ <template #notAuthorized="{ path, style, class: clazz, target, access }">
18
+ <NotAuthorizedAdminButtons v-if="(access?.roles ?? []).includes('writer')"
19
+ :path="urlPath" :style="style" :class="clazz" :object="target"
20
+ :objectType="objectType" :editorRoute="editorRoute"/>
21
+ <NotAuthorized :style="style" :class="clazz" />
22
+ </template>
23
+ </ResolveUrl>
24
+ </template>
25
+
26
+ <script setup>
27
+ import Button from "primevue/button"
28
+ import ContentAdminButtons from "./ContentAdminButtons.vue"
29
+ import NotFoundAdminButtons from "./NotFoundAdminButtons.vue"
30
+ import NotAuthorizedAdminButtons from "./NotAuthorizedAdminButtons.vue"
31
+
32
+ import { ResolveUrl, NotFound, NotAuthorized } from "@live-change/url-frontend"
33
+ import { LimitedAccess } from "@live-change/access-control-frontend";
34
+ import Content from "./Content.vue"
35
+ import Metadata from "./Metadata.vue"
36
+
37
+ import { computed, watch, ref, onMounted } from 'vue'
38
+ import { toRefs } from "@vueuse/core"
39
+
40
+ const isMounted = ref(false)
41
+ onMounted(() => isMounted.value = true)
42
+
43
+ import { path, live, useApi } from '@live-change/vue3-ssr'
44
+ const api = useApi()
45
+ const p = path()
46
+
47
+ const props = defineProps({
48
+ objectType: {
49
+ type: String,
50
+ required: true
51
+ },
52
+ path: {
53
+ type: String,
54
+ required: true
55
+ },
56
+ editorRoute: {
57
+ type: Function,
58
+ default: (objectType, object) => {
59
+ const [service, type] = objectType.split('_')
60
+ const prop = type[0].toLowerCase()+type.slice(1)
61
+ return { name: `${service}:${prop}Editor`, params: { [prop+'Id']: object }}
62
+ }
63
+ },
64
+ class: {},
65
+ style: {}
66
+ })
67
+ const { objectType, path: urlPath, class: clazz, style } = toRefs(props)
68
+
69
+ const urlMore = computed(() => [
70
+ url => p.content.content({ objectType: objectType.value, object: url.target }),
71
+ url => p.content.objectOwnedMetadata({ objectType: objectType.value, object: url.target }),
72
+ url => p.url.targetOwnedCanonical({ targetType: objectType.value, target: url.target })
73
+ ])
74
+
75
+ const canCreateContent = computed(() => api.client.value.roles.includes('writer'))
76
+
77
+ </script>
78
+
79
+ <style scoped>
80
+
81
+ </style>
@@ -8,7 +8,7 @@ import { dbAdminRoutes } from "@live-change/db-admin"
8
8
  import { userRoutes } from "@live-change/user-frontend";
9
9
  import { catchAllPagesRoute, contentEditRoutes, pagesSitemap } from "./components/routes";
10
10
 
11
- export function wysiwygRoutes(config = {}) {
11
+ export function contentRoutes(config = {}) {
12
12
  const { prefix = '/', route = (r) => r } = config
13
13
  return [
14
14
  ...userRoutes({ ...config, prefix: prefix + 'user/' }),
@@ -35,7 +35,7 @@ export async function sitemap(route, api) {
35
35
  export function createRouter(app, config) {
36
36
  const router = _createRouter({
37
37
  history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
38
- routes: wysiwygRoutes(config)
38
+ routes: contentRoutes(config)
39
39
  })
40
40
  return router
41
41
  }
package/index.js CHANGED
@@ -1,17 +1,24 @@
1
- import Content from './front/src/components/Content.vue'
2
- import ContentEditor from './front/src/components/ContentEditor.vue'
3
1
  import Metadata from './front/src/components/Metadata.vue'
4
2
  import MetadataEditor from './front/src/components/MetadataEditor.vue'
3
+ import Content from './front/src/components/Content.vue'
4
+ import ContentEditor from './front/src/components/ContentEditor.vue'
5
+ import ContentPreview from './front/src/components/ContentPreview.vue'
6
+ import ContentSettings from './front/src/components/ContentSettings.vue'
7
+
8
+ export { Metadata, MetadataEditor, Content, ContentEditor, ContentPreview, ContentSettings }
9
+
5
10
  import NotFoundAdminButtons from './front/src/components/NotFoundAdminButtons.vue'
11
+ import NotAuthorizedAdminButtons from "./front/src/components/NotAuthorizedAdminButtons.vue"
12
+ import ContentAdminButtons from './front/src/components/ContentAdminButtons.vue'
13
+ import UrlContent from './front/src/components/UrlContent.vue'
14
+
15
+ export { NotFoundAdminButtons, NotAuthorizedAdminButtons, ContentAdminButtons, UrlContent }
16
+
6
17
  import Page from './front/src/components/Page.vue'
7
18
  import PageEditor from './front/src/components/PageEditor.vue'
8
19
  import PagePreview from './front/src/components/PagePreview.vue'
9
- import PageAdminButtons from './front/src/components/PageAdminButtons.vue'
10
20
 
11
- export {
12
- Content, ContentEditor, Metadata, MetadataEditor, NotFoundAdminButtons, Page, PageEditor, PagePreview,
13
- PageAdminButtons
14
- }
21
+ export { Page, PageEditor, PagePreview }
15
22
 
16
23
  import { catchAllPagesRoute, contentEditRoutes, pagesSitemap } from "./front/src/components/routes"
17
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/content-frontend",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "scripts": {
5
5
  "memDev": "lcli memDev --enableSessions --initScript ./init.js --dbAccess",
6
6
  "localDevInit": "rm tmp.db; lcli localDev --enableSessions --initScript ./init.js",
@@ -21,13 +21,14 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@fortawesome/fontawesome-free": "^6.2.0",
24
- "@live-change/cli": "0.7.5",
24
+ "@live-change/cli": "0.7.6",
25
25
  "@live-change/dao": "0.5.8",
26
26
  "@live-change/dao-vue3": "0.5.8",
27
27
  "@live-change/dao-websocket": "0.5.8",
28
- "@live-change/framework": "0.7.5",
29
- "@live-change/image-service": "0.3.5",
30
- "@live-change/session-service": "0.3.5",
28
+ "@live-change/framework": "0.7.6",
29
+ "@live-change/image-service": "0.3.8",
30
+ "@live-change/session-service": "0.3.8",
31
+ "@live-change/url-frontend": "^0.2.17",
31
32
  "@live-change/vue3-components": "0.2.16",
32
33
  "@live-change/vue3-ssr": "0.2.16",
33
34
  "@vueuse/core": "^9.1.0",
@@ -48,10 +49,10 @@
48
49
  "v-shared-element": "3.1.0",
49
50
  "vue-meta": "^3.0.0-alpha.9",
50
51
  "vue-router": "^4.1.3",
51
- "vue3-scroll-border": "0.1.2"
52
+ "vue3-scroll-border": "0.1.4"
52
53
  },
53
54
  "devDependencies": {
54
- "@live-change/codeceptjs-helper": "0.7.5",
55
+ "@live-change/codeceptjs-helper": "0.7.6",
55
56
  "@wdio/selenium-standalone-service": "^7.20.8",
56
57
  "codeceptjs": "^3.3.4",
57
58
  "generate-password": "1.7.0",
@@ -63,5 +64,5 @@
63
64
  "author": "",
64
65
  "license": "ISC",
65
66
  "description": "",
66
- "gitHead": "c3a1b57517db93829f3a733379a82496651c1039"
67
+ "gitHead": "61ea1e9554f3bf8cb8f9debffdabd8a88855c3b2"
67
68
  }
package/server/init.js CHANGED
@@ -40,7 +40,7 @@ module.exports = async function(services) {
40
40
  id: documentId,
41
41
  ownerType: 'content_Page',
42
42
  owner: pageId,
43
- type: 'content',
43
+ type: 'page',
44
44
  purpose: 'page',
45
45
  version: 1,
46
46
  content: documentContent,