@live-change/content-frontend 0.2.7 → 0.2.9

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.
@@ -0,0 +1,84 @@
1
+ {
2
+ "content": {
3
+ "metadata": {
4
+ "title": "Title",
5
+ "description": "Description",
6
+ "og:title": "Open Graph",
7
+ "og": {
8
+ "title": "Title",
9
+ "description": "Description",
10
+ "image": "Image",
11
+ "determiner": "Determiner",
12
+ "locale": "Locale",
13
+ "localeAlternate:title": "Locale Alternate",
14
+ "type": "Type",
15
+ "type:options": {
16
+ "website": "Website",
17
+ "article": "Article",
18
+ "profile": "Profile",
19
+ "book": "Book",
20
+ "music": {
21
+ "song": "Music Song",
22
+ "album": "Music Album",
23
+ "playlist": "Music Playlist",
24
+ "radio_station": "Music Radio Station"
25
+ },
26
+ "video": {
27
+ "movie": "Video Movie",
28
+ "episode": "Video Episode",
29
+ "tv_show": "Video TV Show",
30
+ "other": "Video Other"
31
+ }
32
+ },
33
+ "music:title": "Music",
34
+ "music": {
35
+ "duration": "Duration",
36
+ "song:title": "Song",
37
+ "creator:title": "Creator",
38
+ "album:title": "Album",
39
+ "album": {
40
+ "url": "URL",
41
+ "disc": "Disc",
42
+ "track": "Track"
43
+ },
44
+ "song": {
45
+ "url": "URL",
46
+ "disc": "Disc",
47
+ "track": "Track"
48
+ },
49
+ "musician:title": "Musician"
50
+ },
51
+ "video:title": "Video",
52
+ "video": {
53
+ "duration": "Duration",
54
+ "actor:title": "Actor",
55
+ "actor": {
56
+ "url": "URL",
57
+ "role": "Role"
58
+ },
59
+ "director:title": "Director",
60
+ "writer:title": "Writer",
61
+ "releaseDate": "Release Date",
62
+ "tag:title": "Tag",
63
+ "series": "Series"
64
+ },
65
+ "article:title": "Article",
66
+ "article": {
67
+ "publishedTime": "Published Time",
68
+ "modifiedTime": "Modified Time",
69
+ "expirationTime": "Expiration Time",
70
+ "author:title": "Authors",
71
+ "section": "Section",
72
+ "tag:title": "Tags"
73
+ },
74
+ "book:title": "Book",
75
+ "book": {
76
+ "author:title": "Authors",
77
+ "isbn": "ISBN",
78
+ "releaseDate": "Release Date",
79
+ "tag:title": "Tags"
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
package/front/src/App.vue CHANGED
@@ -13,11 +13,14 @@
13
13
  import ViewRoot from "@live-change/frontend-base/ViewRoot.vue"
14
14
  import NavBar from "./NavBar.vue"
15
15
 
16
+ import { useI18n } from 'vue-i18n'
17
+ const i18n = useI18n()
18
+
16
19
  import { useMeta } from 'vue-meta'
17
20
  const { meta } = useMeta({
18
21
  title: 'Title',
19
22
  htmlAttrs: {
20
- lang: 'en',
23
+ lang: i18n.locale.value,
21
24
  amp: true
22
25
  }
23
26
  })
@@ -0,0 +1,134 @@
1
+ <template>
2
+ <!-- <pre>{{ JSON.stringify(metadata, null, " ") }}</pre>-->
3
+ </template>
4
+
5
+ <script setup>
6
+ const props = defineProps({
7
+ objectType: {
8
+ type: String,
9
+ required: true
10
+ },
11
+ object: {
12
+ type: String,
13
+ required: true
14
+ },
15
+ url: {
16
+ type: Object,
17
+ required: true
18
+ }
19
+ })
20
+
21
+ import { computed, ref, onMounted } from 'vue'
22
+ import { path, live } from '@live-change/vue3-ssr'
23
+ import { useHost } from "@live-change/frontend-base"
24
+ const host = useHost()
25
+
26
+ const p = path()
27
+
28
+ const metadataLivePath = computed(
29
+ () => p.content.objectOwnedMetadata({ objectType: props.objectType, object: props.object })
30
+ .with(metadata => p.image.image({ image: metadata.og.image }).bind('ogImage'))
31
+ )
32
+ const canonicalUrlLivePath = computed(
33
+ () => p.url.targetOwnedCanonical({ targetType: props.objectType, target: props.object })
34
+ )
35
+ const [metadata, canonical] = await Promise.all([
36
+ live(metadataLivePath),
37
+ live(canonicalUrlLivePath)
38
+ ])
39
+
40
+ function metaProperty(name, value) {
41
+ return value ? { property: name, content: value } : undefined
42
+ }
43
+ function metaProperties(name, value) {
44
+ if(!value) return []
45
+ return value.map(v => ({ property: name, content: v }))
46
+ }
47
+ function metaPropertiesObjects(name, value) {
48
+ if(!value) return []
49
+ return value.map(v => {
50
+ for(let key in v) {
51
+ return { property: name+':'+key, content: v[key] }
52
+ }
53
+ })
54
+ }
55
+ import { useMeta } from 'vue-meta'
56
+ const m = metadata.value
57
+ const canonicalUrlDomain = canonical.value?.domain || host
58
+ const canonicalUrl = `https://${canonicalUrlDomain}/${canonical.value?.path ?? ''}`
59
+
60
+ 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
+ }
70
+
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
+ })
130
+ </script>
131
+
132
+ <style scoped>
133
+
134
+ </style>
@@ -1,13 +1,19 @@
1
1
  <template>
2
- <auto-input v-model="editable.title" :definition="properties.title" />
3
-
2
+ <!-- <pre style="white-space: pre-wrap; word-wrap: break-word;">{{ JSON.stringify(editable, null, ' ') }}</pre>-->
3
+ <auto-editor :definition="editableDefinition" v-model="editable" :rootValue="editable" i18n="content.metadata." />
4
+ <Button label="Save metadata" icon="pi pi-save" :disabled="!changed || error" @click="save" />
5
+ <div>
6
+ <small v-if="error" class="p-error">Fix errors above to save</small>
7
+ </div>
4
8
  </template>
5
9
 
6
10
  <script setup>
11
+ import Button from 'primevue/button'
12
+ import { AutoInput, AutoField, AutoEditor } from '@live-change/frontend-auto-form'
7
13
 
8
- import AutoInput from '@live-change/frontend-auto-form/AutoInput.vue'
14
+ import "@live-change/image-frontend"
9
15
 
10
- import { computed, watch, ref, onMounted, inject } from 'vue'
16
+ import { computed, watch, ref, onMounted, onUnmounted, inject } from 'vue'
11
17
  import { toRefs } from "@vueuse/core"
12
18
 
13
19
  const isMounted = ref(false)
@@ -31,11 +37,25 @@
31
37
  const p = path()
32
38
 
33
39
  const definition = api.getServiceDefinition('content').models.Metadata
34
- const properties = definition.properties
35
-
36
- import { synchronized } from "@live-change/vue3-components"
37
-
38
- const metadata = await live(p.content.objectOwnedMetadata({ objectType, object }))
40
+ const editableDefinition = {
41
+ ...definition,
42
+ properties: { ...{
43
+ ...definition.properties,
44
+ objectType: undefined,
45
+ object: undefined,
46
+ lastUpdate: undefined
47
+ } }
48
+ }
49
+
50
+ import { useToast } from 'primevue/usetoast'
51
+ const toast = useToast()
52
+ import { useConfirm } from 'primevue/useconfirm'
53
+ const confirm = useConfirm()
54
+
55
+ import { synchronized, defaultData, validateData } from "@live-change/vue3-components"
56
+
57
+ const serverMetadata = await live(p.content.objectOwnedMetadata({ objectType, object }))
58
+ const metadata = computed(() => serverMetadata.value || defaultData(editableDefinition))
39
59
 
40
60
  const synchronizedMetadata = synchronized({
41
61
  source: metadata,
@@ -43,13 +63,22 @@
43
63
  identifiers: { object, objectType },
44
64
  recursive: true,
45
65
  autoSave: false,
46
- onSave: () => toast.add({ severity: 'info', summary: 'Public access saved', life: 1500 })
47
- }).value
66
+ onSave: () => toast.add({ severity: 'info', summary: 'Metadata saved', life: 1500 })
67
+ })
48
68
 
49
69
  const editable = synchronizedMetadata.value
50
70
  const save = synchronizedMetadata.save
51
71
  const changed = synchronizedMetadata.changed
52
72
 
73
+ const error = computed(() => validateData(editableDefinition, editable.value))
74
+
75
+
76
+ function beforeUnload(ev) {
77
+ if(changed.value) return ev.returnValue = "You have some unsaved changes!"
78
+ }
79
+ onMounted(() => window.addEventListener('beforeunload', beforeUnload))
80
+ onUnmounted(() => window.removeEventListener('beforeunload', beforeUnload))
81
+
53
82
  </script>
54
83
 
55
84
  <style scoped>
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <ResolveUrl targetType="content_Page" :path="urlPath" :fetchMore="urlMore">
3
3
  <template #default="{ target, style, class: clazz }">
4
+ <Metadata objectType="content_Page" :object="target" />
4
5
  <LimitedAccess :requiredRoles="['writer']" objectType="content_Page" :object="target" hidden>
5
6
  <PageAdminButtons :page="target" :style="style" :class="clazz" :name="urlPath.value" />
6
7
  </LimitedAccess>
@@ -21,6 +22,7 @@
21
22
  import { ResolveUrl, NotFound } from "@live-change/url-frontend"
22
23
  import { LimitedAccess } from "@live-change/access-control-frontend";
23
24
  import Content from "./Content.vue"
25
+ import Metadata from "./Metadata.vue"
24
26
 
25
27
  import { computed, watch, ref, onMounted } from 'vue'
26
28
  import { toRefs } from "@vueuse/core"
@@ -34,7 +36,9 @@
34
36
 
35
37
  const urlMore = [
36
38
  url => p.content.page({ page: url.target }),
37
- url => p.content.content({ objectType: 'content_Page', object: 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 })
38
42
  ]
39
43
 
40
44
  const canCreatePage = computed(() => api.client.value.roles.includes('writer'))
@@ -14,14 +14,15 @@
14
14
  </AccordionTab>
15
15
  <AccordionTab>
16
16
  <template #header>
17
- <span v-if="metadata" class="font-bold mr-1">Title: {{ metadata.title }}</span>
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>
18
19
  <span v-else class="font-bold text-red-600">Metadata not set</span>
19
20
  </template>
20
21
  <MetadataEditor objectType="content_Page" :object="pageId" :key="pageId"></MetadataEditor>
21
22
  </AccordionTab>
22
23
  </Accordion>
23
24
 
24
- <DocumentEditor v-if="pageData" targetType="content_Page" :target="pageId"
25
+ <DocumentEditor v-if="pageData" targetType="content_Page" :target="pageId" purpose="page"
25
26
  :config="contentConfig" type="content" v-model:saveState="saveState" v-model:version="version">
26
27
  <template #menuEnd="{}">
27
28
 
@@ -0,0 +1,13 @@
1
+ import deepmerge from 'deepmerge';
2
+
3
+ import contentEn from "../locales/en.json"
4
+ import { locales as autoFormLocales } from "@live-change/frontend-auto-form"
5
+
6
+ export default {
7
+ i18nMessages: {
8
+ en: deepmerge.all([
9
+ contentEn,
10
+ autoFormLocales.en
11
+ ])
12
+ }
13
+ }
@@ -1,6 +1,6 @@
1
1
  import { clientEntry } from '@live-change/frontend-base/client-entry.js'
2
2
  import App from './App.vue'
3
3
  import { createRouter } from './router'
4
+ import config from './config.js'
4
5
 
5
-
6
- clientEntry(App, createRouter)
6
+ await clientEntry(App, createRouter, config)
@@ -1,6 +1,7 @@
1
1
  import { serverEntry } from '@live-change/frontend-base/server-entry.js'
2
2
  import App from './App.vue'
3
3
  import { createRouter } from './router'
4
+ import config from './config.js'
4
5
 
5
- const render = serverEntry(App, createRouter)
6
+ const render = serverEntry(App, createRouter, config)
6
7
  export { render }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/content-frontend",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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",
@@ -20,7 +20,7 @@
20
20
  "debug": "node --inspect-brk server"
21
21
  },
22
22
  "dependencies": {
23
- "@fortawesome/fontawesome-free": "^6.1.1",
23
+ "@fortawesome/fontawesome-free": "^6.2.0",
24
24
  "@live-change/cli": "0.7.4",
25
25
  "@live-change/dao": "0.5.8",
26
26
  "@live-change/dao-vue3": "0.5.8",
@@ -28,8 +28,8 @@
28
28
  "@live-change/framework": "0.7.4",
29
29
  "@live-change/image-service": "0.3.2",
30
30
  "@live-change/session-service": "0.3.2",
31
- "@live-change/vue3-components": "0.2.15",
32
- "@live-change/vue3-ssr": "0.2.15",
31
+ "@live-change/vue3-components": "0.2.16",
32
+ "@live-change/vue3-ssr": "0.2.16",
33
33
  "@tiptap/extension-highlight": "^2.0.0-beta.33",
34
34
  "@tiptap/extension-underline": "2.0.0-beta.23",
35
35
  "@tiptap/starter-kit": "^2.0.0-beta.185",
@@ -38,6 +38,7 @@
38
38
  "codeceptjs-assert": "^0.0.5",
39
39
  "compression": "^1.7.4",
40
40
  "cross-env": "^7.0.3",
41
+ "deepmerge": "^4.2.2",
41
42
  "get-port-sync": "1.0.1",
42
43
  "pica": "^9.0.1",
43
44
  "pretty-bytes": "^6.0.0",
@@ -66,5 +67,5 @@
66
67
  "author": "",
67
68
  "license": "ISC",
68
69
  "description": "",
69
- "gitHead": "ef577fb31dd857acb491bd7114e611638283fe6f"
70
+ "gitHead": "703c9723562ade7b2420e1b25b28ced51a96db09"
70
71
  }
package/server/init.js CHANGED
@@ -96,6 +96,40 @@ module.exports = async function(services) {
96
96
  target: 'one'
97
97
  })
98
98
 
99
+ await services.content.models.Metadata.create({
100
+ id: App.encodeIdentifier(['content_Page', 'one']),
101
+ objectType: 'content_Page',
102
+ object: 'one',
103
+ title: 'Test Page',
104
+ description: 'Test Description',
105
+ "og": {
106
+ "locale": "en_US",
107
+ "localeAlternate": [],
108
+ "type": "website",
109
+ "music": {
110
+ "song": [],
111
+ "album": [],
112
+ "musician": [],
113
+ "creator": []
114
+ },
115
+ "video": {
116
+ "actor": [],
117
+ "director": [],
118
+ "writer": [],
119
+ "tag": []
120
+ },
121
+ "article": {
122
+ "author": [],
123
+ "tag": []
124
+ },
125
+ "profile": {},
126
+ "book": {
127
+ "author": [],
128
+ "tag": []
129
+ }
130
+ }
131
+ })
132
+
99
133
  await createPage('two')
100
134
 
101
135
  await services.url.models.Canonical.create({