@tailor-cms/tce-preview-runtime 0.2.2 → 0.4.0-beta.10

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.
package/package.json CHANGED
@@ -1,20 +1,24 @@
1
1
  {
2
2
  "name": "@tailor-cms/tce-preview-runtime",
3
- "version": "0.2.2",
3
+ "version": "0.4.0-beta.10",
4
4
  "dependencies": {
5
+ "@lukeed/uuid": "^2.0.1",
5
6
  "@mdi/font": "7.2.96",
6
7
  "@vitejs/plugin-vue": "4.3.4",
7
8
  "@vueuse/core": "10.4.1",
9
+ "date-fns": "^3.2.0",
8
10
  "ky": "1.0.1",
11
+ "lodash": "^4.17.21",
9
12
  "sass": "1.68.0",
10
13
  "split.js": "1.6.5",
11
14
  "typescript": "5.2.2",
15
+ "universal-cookie": "^6.1.3",
12
16
  "vite": "4.4.9",
13
17
  "vite-plugin-vuetify": "1.0.2",
14
18
  "vue": "3.3.4",
15
19
  "vue-tsc": "1.8.13",
16
20
  "vue3-ts-jsoneditor": "2.9.0",
17
- "vuetify": "3.3.17"
21
+ "vuetify": "3.4.0"
18
22
  },
19
23
  "devDependencies": {
20
24
  "@tailor-cms/eslint-config": "0.0.2"
@@ -26,7 +30,7 @@
26
30
  "dev": "vite",
27
31
  "build": "vue-tsc --noEmit && vite build",
28
32
  "serve": "vite preview",
29
- "lint": "eslint --ext .js,.ts,.vue .",
33
+ "lint": "eslint --ext .ts,.vue .",
30
34
  "lint:fix": "pnpm lint --fix"
31
35
  }
32
36
  }
package/src/App.vue CHANGED
@@ -5,15 +5,33 @@ import ky from 'ky';
5
5
  import AppBar from './components/AppBar.vue';
6
6
  import MainLayout from './components/MainLayout.vue';
7
7
 
8
- const SERVER_HOST = `localhost:${import.meta.env.VITE_TCE_SERVER_PORT || 8030}`;
9
- const api = ky.create({ prefixUrl: `http://${SERVER_HOST}` });
10
- const ws = new WebSocket(`ws://${SERVER_HOST}`);
8
+ interface ContentElement {
9
+ id: number;
10
+ uid: string;
11
+ }
12
+
13
+ const appUrl = new URL(window.location.href);
14
+ const apiPrefix = '/tce-server';
15
+ const api = ky.create({ prefixUrl: apiPrefix });
16
+ const wsProtocol = appUrl.protocol === 'http:' ? 'ws:' : 'wss:';
17
+ const ws = new WebSocket(`${wsProtocol}//${appUrl.host}${apiPrefix}`);
18
+
19
+ const element = ref<ContentElement>();
20
+ const userState = ref({});
11
21
 
12
- const element = ref({});
13
22
  onMounted(async () => {
14
- element.value = await getElement();
15
- ws.addEventListener('message', (event) => {
16
- element.value = JSON.parse(event.data);
23
+ await getElement();
24
+ ws.addEventListener('message', (message) => {
25
+ const event = JSON.parse(message.data);
26
+ const elementUid = element.value?.uid;
27
+ if (elementUid && elementUid !== event.entityId) return;
28
+ if (event.type === 'userState:update') {
29
+ userState.value = event.payload;
30
+ }
31
+ if (event.type === 'element:update') {
32
+ element.value = event.payload;
33
+ }
34
+ if (event.type === 'userContext:change') getElement();
17
35
  });
18
36
  });
19
37
 
@@ -21,10 +39,12 @@ function timeout(ms: number) {
21
39
  return new Promise((resolve) => setTimeout(resolve, ms));
22
40
  }
23
41
 
24
- async function getElement() {
42
+ async function getElement(): Promise<any> {
25
43
  try {
26
- const response: any = await api('content-element').json();
27
- return response?.element || {};
44
+ const res = await api('content-element').json();
45
+ const { element: elementVal, userState: userStateVal } = res as any;
46
+ element.value = elementVal;
47
+ userState.value = userStateVal;
28
48
  } catch (error) {
29
49
  console.log('Error on element get', error);
30
50
  await timeout(2000);
@@ -32,12 +52,33 @@ async function getElement() {
32
52
  return getElement();
33
53
  }
34
54
  }
55
+
56
+ async function resetElement() {
57
+ const id = element.value?.id;
58
+ if (!id) return;
59
+ await resetState();
60
+ const path = `content-element/${id}/reset-element`;
61
+ return api.post(path);
62
+ }
63
+
64
+ async function resetState() {
65
+ const id = element.value?.id;
66
+ if (!id) return;
67
+ const path = `content-element/${id}/reset-state`;
68
+ return api.post(path);
69
+ }
35
70
  </script>
36
71
 
37
72
  <template>
38
73
  <v-app>
39
74
  <AppBar />
40
- <MainLayout :element="element" class="mt-14" />
75
+ <MainLayout
76
+ :element="element"
77
+ :user-state="userState"
78
+ class="mt-14"
79
+ @reset-element="resetElement"
80
+ @reset-state="resetState"
81
+ />
41
82
  </v-app>
42
83
  </template>
43
84
 
@@ -2,11 +2,11 @@
2
2
  import logoUrl from '../assets/logo.png';
3
3
  import { useGlobalState } from '../state';
4
4
 
5
- const { isDark, showEdit, showDisplay, showBothComponents } = useGlobalState();
5
+ const { showEdit, showDisplay, showBothComponents } = useGlobalState();
6
6
  </script>
7
7
 
8
8
  <template>
9
- <v-app-bar :color="isDark ? 'grey-darken-4' : 'grey-lighten-4'" elevation="1">
9
+ <v-app-bar color="blue-grey-darken-4" elevation="6">
10
10
  <template #prepend>
11
11
  <v-app-bar-nav-icon class="ml-2">
12
12
  <img :src="logoUrl" alt="Tailor logo" width="36" />
@@ -16,25 +16,21 @@ const { isDark, showEdit, showDisplay, showBothComponents } = useGlobalState();
16
16
  Content Element Kit
17
17
  <v-chip
18
18
  class="ml-3 mb-1 text-body-2"
19
- color="teal-accent-4"
19
+ color="#23F48B"
20
20
  density="compact"
21
21
  variant="tonal"
22
22
  >
23
- 0.3.0
23
+ 0.4.0
24
24
  </v-chip>
25
25
  </v-app-bar-title>
26
26
  <v-spacer />
27
- <v-btn-toggle
28
- :variant="isDark ? 'flat' : 'tonal'"
29
- class="mr-4"
30
- density="compact"
31
- >
27
+ <v-btn-toggle class="mr-4" density="compact">
32
28
  <v-btn @click="showEdit">
33
- <span>Edit</span>
29
+ <span>Authoring</span>
34
30
  <v-icon end>mdi-pencil</v-icon>
35
31
  </v-btn>
36
32
  <v-btn @click="showDisplay">
37
- <span>Display</span>
33
+ <span>End-user</span>
38
34
  <v-icon end>mdi-fullscreen</v-icon>
39
35
  </v-btn>
40
36
  <v-btn @click="showBothComponents">
@@ -1,26 +1,75 @@
1
1
  <template>
2
2
  <div v-show="isLoaded">
3
- <v-tabs density="compact" hide-slider>
4
- <v-tab>Server state</v-tab>
3
+ <v-tabs v-model="state.selectedTab">
4
+ <v-tab :value="1">
5
+ <VChip append-icon="mdi-pen" class="mr-2" color="indigo" size="small">
6
+ {{ state.elementHistory.length }}
7
+ </VChip>
8
+ Authoring history
9
+ </v-tab>
10
+ <v-tab :value="2">
11
+ <VChip
12
+ append-icon="mdi-account"
13
+ class="mr-2"
14
+ color="green"
15
+ size="small"
16
+ >
17
+ {{ state.userInteractions.length }}
18
+ </VChip>
19
+ End-User state history
20
+ </v-tab>
5
21
  </v-tabs>
6
22
  <div class="pa-6">
7
- <JsonEditor
8
- v-if="isLoaded"
9
- :main-menu-bar="false"
10
- :status-bar="false"
11
- :value="orderKeys(props.element)"
12
- mode="text"
13
- />
23
+ <v-window v-model="state.selectedTab">
24
+ <v-window-item :value="1">
25
+ <VBtn
26
+ class="mb-4"
27
+ color="indigo-darken-4"
28
+ variant="tonal"
29
+ @click="$emit('resetElement')"
30
+ >
31
+ <VIcon class="mr-2 left">mdi-refresh</VIcon>
32
+ Reset content element
33
+ </VBtn>
34
+ <MutationList :changes="state.elementHistory" />
35
+ </v-window-item>
36
+ <v-window-item :value="2">
37
+ <VBtn
38
+ class="mb-4"
39
+ color="indigo-darken-4"
40
+ variant="tonal"
41
+ @click="$emit('resetState')"
42
+ >
43
+ <VIcon class="mr-2 left">mdi-refresh</VIcon>
44
+ Reset state context
45
+ </VBtn>
46
+ <MutationList :changes="state.userInteractions" />
47
+ </v-window-item>
48
+ </v-window>
14
49
  </div>
15
50
  </div>
16
51
  </template>
17
52
 
18
53
  <script setup lang="ts">
19
- import JsonEditor from 'vue3-ts-jsoneditor';
54
+ import { reactive, watch } from 'vue';
55
+ import { format as formatDate } from 'date-fns';
20
56
 
21
- const props = defineProps<{ element: any; isLoaded: Boolean }>();
57
+ import MutationList from './MutationList.vue';
22
58
 
23
- const orderKeys = (val: any) => {
59
+ defineEmits(['resetElement', 'resetState']);
60
+ const props = defineProps<{
61
+ isLoaded: Boolean;
62
+ element: any;
63
+ userState: any;
64
+ }>();
65
+
66
+ const state = reactive({
67
+ selectedTab: 1,
68
+ elementHistory: [] as any[],
69
+ userInteractions: [] as any[],
70
+ });
71
+
72
+ const orderContentElementKeys = (val: any) => {
24
73
  return [
25
74
  'id',
26
75
  'uid',
@@ -41,4 +90,31 @@ const orderKeys = (val: any) => {
41
90
  return acc;
42
91
  }, {});
43
92
  };
93
+
94
+ watch(
95
+ () => props.element,
96
+ (val) => {
97
+ if (val.updatedAt === state.elementHistory[0]?.data?.updatedAt) return;
98
+ state.elementHistory.unshift({
99
+ id: state.elementHistory.length,
100
+ title: formatDate(val.updatedAt, 'HH:mm:ss'),
101
+ data: orderContentElementKeys(val),
102
+ });
103
+ state.selectedTab = 1;
104
+ },
105
+ { deep: true },
106
+ );
107
+
108
+ watch(
109
+ () => props.userState,
110
+ (val) => {
111
+ state.userInteractions.unshift({
112
+ id: state.userInteractions.length,
113
+ title: formatDate(new Date(), 'HH:mm:ss'),
114
+ data: val,
115
+ });
116
+ state.selectedTab = 2;
117
+ },
118
+ { deep: true },
119
+ );
44
120
  </script>
@@ -7,7 +7,8 @@ import PreviewPanel from './PreviewPanel.vue';
7
7
  import SplashLoader from './SplashLoader.vue';
8
8
  import { useGlobalState } from '../state';
9
9
 
10
- const props = defineProps<{ element: any }>();
10
+ defineEmits(['resetElement', 'resetState']);
11
+ const props = defineProps<{ element: any; userState: any }>();
11
12
 
12
13
  const { isDark } = useGlobalState();
13
14
  const isLoaded = ref(false);
@@ -51,7 +52,10 @@ onMounted(() => {
51
52
  id="panelBottom"
52
53
  :element="props.element"
53
54
  :is-loaded="isLoaded"
55
+ :user-state="props.userState"
54
56
  class="preview-panel-bottom"
57
+ @reset-element="$emit('resetElement')"
58
+ @reset-state="$emit('resetState')"
55
59
  />
56
60
  </main>
57
61
  </template>
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <VDataIterator :items="changes" :items-per-page="50" expand-on-click>
3
+ <template #default="{ items, isExpanded, toggleExpand }">
4
+ <template v-for="(item, index) in items" :key="item.raw.id">
5
+ <v-card
6
+ :disabled="index === 0"
7
+ :ripple="false"
8
+ class="mb-1"
9
+ @click="toggleExpand(item)"
10
+ >
11
+ <v-card-title class="d-flex align-center">
12
+ <v-icon color="orange-darken-3" icon="mdi-update" size="18" start />
13
+ <div class="text-body-1">{{ item.raw.title }}</div>
14
+ </v-card-title>
15
+ <v-card-text v-if="isExpanded(item) || index === 0">
16
+ <JsonEditor
17
+ :main-menu-bar="false"
18
+ :status-bar="false"
19
+ :value="item.raw.data"
20
+ mode="text"
21
+ />
22
+ </v-card-text>
23
+ </v-card>
24
+ </template>
25
+ </template>
26
+ </VDataIterator>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ import JsonEditor from 'vue3-ts-jsoneditor';
31
+
32
+ defineProps<{ changes: any[] }>();
33
+ </script>
34
+
35
+ <style scoped lang="scss">
36
+ .v-card--disabled > :not(.v-card__loader) {
37
+ opacity: 1 !important;
38
+ }
39
+ </style>
@@ -3,10 +3,9 @@
3
3
  <div :id="PANELS.EDIT" class="d-flex flex-1-1-50">
4
4
  <iframe
5
5
  :key="`author-${isLoaded ? new Date().getTime() : 0}`"
6
+ :src="VITE_EDIT_RUNTIME_URL"
6
7
  class="flex-1-1-100"
7
8
  frameBorder="0"
8
- sandbox="allow-scripts"
9
- src="http://localhost:8010"
10
9
  title="Edit component container"
11
10
  >
12
11
  </iframe>
@@ -14,10 +13,9 @@
14
13
  <div :id="PANELS.DISPLAY" class="d-flex flex-1-1-50">
15
14
  <iframe
16
15
  :key="`display-${isLoaded ? new Date().getTime() : 0}`"
16
+ :src="VITE_DISPLAY_RUNTIME_URL"
17
17
  class="flex-1-1-100"
18
18
  frameBorder="0"
19
- sandbox="allow-scripts"
20
- src="http://localhost:8020"
21
19
  title="Display component container"
22
20
  >
23
21
  </iframe>
@@ -31,6 +29,7 @@ import Split from 'split.js';
31
29
 
32
30
  import { PANELS, useGlobalState } from '../state';
33
31
 
32
+ const { VITE_DISPLAY_RUNTIME_URL, VITE_EDIT_RUNTIME_URL } = import.meta.env;
34
33
  const { previewPanelSplit } = useGlobalState();
35
34
 
36
35
  defineProps<{ isLoaded: Boolean }>();
@@ -6,13 +6,13 @@
6
6
  <img :src="logo" alt="Logo" width="88" />
7
7
  </div>
8
8
  <div class="splash-loader__text text-body-1">
9
- <div>Booting Teaching Element Kit....</div>
9
+ <div>Booting Content Element Kit....</div>
10
10
  <v-progress-linear
11
11
  v-if="isFirstBoot"
12
12
  :model-value="progress"
13
13
  class="my-3"
14
14
  />
15
- <div class="font-weight-bold">Beta preview v0.3.0</div>
15
+ <div class="font-weight-bold">Beta preview v0.4.0</div>
16
16
  </div>
17
17
  </div>
18
18
  </div>
package/src/main.ts CHANGED
@@ -1,11 +1,18 @@
1
1
  import '@mdi/font/css/materialdesignicons.css';
2
2
  import 'vuetify/styles';
3
3
 
4
+ import Cookies from 'universal-cookie';
4
5
  import { createApp } from 'vue';
5
6
  import { createVuetify } from 'vuetify';
7
+ import { v4 as uuid } from '@lukeed/uuid/secure';
6
8
 
7
9
  import App from './App.vue';
8
10
 
11
+ const CLIENT_ID_KEY = 'cekClientId';
12
+
13
+ const cookies = new Cookies(null, { path: '/' });
14
+ if (!cookies.get(CLIENT_ID_KEY)) cookies.set(CLIENT_ID_KEY, uuid());
15
+
9
16
  createApp(App)
10
17
  .use(createVuetify({ icons: { defaultSet: 'mdi' } }))
11
18
  .mount('#app');
package/vite.config.ts CHANGED
@@ -1,14 +1,36 @@
1
1
  import { defineConfig } from 'vite';
2
+ import pick from 'lodash/pick';
2
3
  import vue from '@vitejs/plugin-vue';
3
4
  import vuetify from 'vite-plugin-vuetify';
4
5
 
6
+ const config = pick(process.env, [
7
+ 'EDIT_RUNTIME_URL',
8
+ 'DISPLAY_RUNTIME_URL',
9
+ 'SERVER_RUNTIME_URL',
10
+ 'PREVIEW_RUNTIME_PORT',
11
+ ]);
12
+
13
+ // Expose to Frontend by prefixing with VITE_
14
+ Object.entries(config).forEach(
15
+ ([k, v]) => (process.env[`VITE_${k}`] = v as string),
16
+ );
17
+
5
18
  // https://vitejs.dev/config/
6
19
  export default defineConfig((): any => {
7
20
  return {
8
21
  root: './src',
9
22
  server: {
10
- host: '0.0.0.0', // Accept connections from any host (Docker)
11
- port: 8080,
23
+ // Accept connections from any host (Docker)
24
+ host: '0.0.0.0',
25
+ port: parseInt(config.PREVIEW_RUNTIME_PORT, 10),
26
+ proxy: {
27
+ '/tce-server': {
28
+ target: config.SERVER_RUNTIME_URL,
29
+ ws: true,
30
+ changeOrigin: true,
31
+ rewrite: (path) => path.replace(/^\/tce-server/, ''),
32
+ },
33
+ },
12
34
  },
13
35
  logLevel: 'warn',
14
36
  optimizeDeps: { entries: ['./src/main.js'] },