@byline/host-tanstack-start 4.15.0 → 4.17.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 (183) hide show
  1. package/dist/admin-shell/chrome/admin-menu-drawer.d.ts +2 -1
  2. package/dist/admin-shell/chrome/admin-menu-drawer.js +17 -5
  3. package/dist/admin-shell/chrome/dashboard.js +41 -5
  4. package/dist/admin-shell/chrome/dashboard.test.d.ts +8 -0
  5. package/dist/admin-shell/collections/api.d.ts +2 -2
  6. package/dist/admin-shell/collections/create.d.ts +2 -2
  7. package/dist/admin-shell/collections/create.js +1 -0
  8. package/dist/admin-shell/collections/edit.d.ts +2 -2
  9. package/dist/admin-shell/collections/edit.js +1 -0
  10. package/dist/admin-shell/collections/history.d.ts +4 -12
  11. package/dist/admin-shell/collections/history.js +112 -303
  12. package/dist/admin-shell/collections/reindex-button.d.ts +1 -1
  13. package/dist/admin-shell/collections/reindex-button.js +5 -1
  14. package/dist/admin-shell/collections/restore-version-modal.d.ts +4 -4
  15. package/dist/admin-shell/collections/restore-version-modal.js +8 -24
  16. package/dist/admin-shell/collections/version-history.d.ts +72 -0
  17. package/dist/admin-shell/collections/version-history.js +259 -0
  18. package/dist/admin-shell/singletons/history.d.ts +19 -0
  19. package/dist/admin-shell/singletons/history.js +170 -0
  20. package/dist/admin-shell/singletons/history.test.d.ts +8 -0
  21. package/dist/admin-shell/singletons/preview-link.d.ts +12 -0
  22. package/dist/admin-shell/singletons/preview-link.js +54 -0
  23. package/dist/admin-shell/singletons/view-menu.d.ts +13 -0
  24. package/dist/admin-shell/singletons/view-menu.js +116 -0
  25. package/dist/admin-shell/singletons/view.d.ts +18 -0
  26. package/dist/admin-shell/singletons/view.js +259 -0
  27. package/dist/admin-shell/singletons/view.test.d.ts +8 -0
  28. package/dist/integrations/analytics-agent.d.ts +20 -0
  29. package/dist/integrations/analytics-agent.js +57 -0
  30. package/dist/integrations/analytics-events.d.ts +51 -0
  31. package/dist/integrations/analytics-events.js +129 -0
  32. package/dist/integrations/api-utils.d.ts +18 -4
  33. package/dist/integrations/api-utils.js +10 -3
  34. package/dist/integrations/serve-analytics-agent.d.ts +8 -0
  35. package/dist/integrations/serve-analytics-agent.js +11 -0
  36. package/dist/routes/analytics-range.d.ts +12 -0
  37. package/dist/routes/analytics-range.js +35 -0
  38. package/dist/routes/create-admin-analytics-route.d.ts +8 -0
  39. package/dist/routes/create-admin-analytics-route.js +108 -0
  40. package/dist/routes/create-admin-dashboard-route.js +2 -2
  41. package/dist/routes/create-admin-layout-route.js +10 -4
  42. package/dist/routes/create-collection-api-route.js +3 -3
  43. package/dist/routes/create-collection-create-route.js +4 -3
  44. package/dist/routes/create-collection-edit-route.js +4 -3
  45. package/dist/routes/create-collection-history-route.js +4 -3
  46. package/dist/routes/create-collection-list-route.js +4 -3
  47. package/dist/routes/create-singleton-history-route.d.ts +8 -0
  48. package/dist/routes/create-singleton-history-route.js +99 -0
  49. package/dist/routes/create-singleton-route.d.ts +35 -0
  50. package/dist/routes/create-singleton-route.js +130 -0
  51. package/dist/routes/get-multi-collection-definition.d.ts +10 -0
  52. package/dist/routes/get-multi-collection-definition.js +6 -0
  53. package/dist/routes/get-singleton-definition.d.ts +10 -0
  54. package/dist/routes/get-singleton-definition.js +6 -0
  55. package/dist/routes/index.d.ts +3 -0
  56. package/dist/routes/index.js +3 -0
  57. package/dist/server-fns/actors.d.ts +16 -0
  58. package/dist/server-fns/actors.js +25 -0
  59. package/dist/server-fns/admin-document-presentation.d.ts +30 -0
  60. package/dist/server-fns/admin-document-presentation.js +32 -0
  61. package/dist/server-fns/analytics/index.d.ts +9 -0
  62. package/dist/server-fns/analytics/index.js +2 -0
  63. package/dist/server-fns/analytics/maintenance.d.ts +22 -0
  64. package/dist/server-fns/analytics/maintenance.js +42 -0
  65. package/dist/server-fns/analytics/queries.d.ts +25 -0
  66. package/dist/server-fns/analytics/queries.js +54 -0
  67. package/dist/server-fns/collections/actors.d.ts +1 -10
  68. package/dist/server-fns/collections/actors.js +1 -25
  69. package/dist/server-fns/collections/get.js +9 -26
  70. package/dist/server-fns/collections/reorder.js +1 -1
  71. package/dist/server-fns/collections/scheduled-publication.js +2 -2
  72. package/dist/server-fns/collections/upload.js +3 -3
  73. package/dist/server-fns/collections/utils.d.ts +1 -7
  74. package/dist/server-fns/collections/utils.js +1 -4
  75. package/dist/server-fns/serialise.d.ts +14 -0
  76. package/dist/server-fns/serialise.js +4 -0
  77. package/dist/server-fns/singleton-document-read.d.ts +20 -0
  78. package/dist/server-fns/singleton-document-read.js +10 -0
  79. package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +12 -0
  80. package/dist/server-fns/singletons/cancel-scheduled-publish.js +7 -0
  81. package/dist/server-fns/singletons/change-status.d.ts +14 -0
  82. package/dist/server-fns/singletons/change-status.js +7 -0
  83. package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +14 -0
  84. package/dist/server-fns/singletons/confirm-scheduled-publish.js +9 -0
  85. package/dist/server-fns/singletons/copy-to-locale.d.ts +18 -0
  86. package/dist/server-fns/singletons/copy-to-locale.js +11 -0
  87. package/dist/server-fns/singletons/find-by-version.d.ts +16 -0
  88. package/dist/server-fns/singletons/find-by-version.js +9 -0
  89. package/dist/server-fns/singletons/get-scheduled-publish.d.ts +12 -0
  90. package/dist/server-fns/singletons/get-scheduled-publish.js +7 -0
  91. package/dist/server-fns/singletons/get.d.ts +24 -0
  92. package/dist/server-fns/singletons/get.js +6 -0
  93. package/dist/server-fns/singletons/history.d.ts +31 -0
  94. package/dist/server-fns/singletons/history.js +22 -0
  95. package/dist/server-fns/singletons/index.d.ts +19 -0
  96. package/dist/server-fns/singletons/index.js +12 -0
  97. package/dist/server-fns/singletons/restore-version.d.ts +14 -0
  98. package/dist/server-fns/singletons/restore-version.js +7 -0
  99. package/dist/server-fns/singletons/schedule-publish.d.ts +16 -0
  100. package/dist/server-fns/singletons/schedule-publish.js +10 -0
  101. package/dist/server-fns/singletons/unpublish.d.ts +12 -0
  102. package/dist/server-fns/singletons/unpublish.js +7 -0
  103. package/dist/server-fns/singletons/update.d.ts +14 -0
  104. package/dist/server-fns/singletons/update.js +14 -0
  105. package/dist/vite/analytics-dev.d.ts +22 -0
  106. package/dist/vite/analytics-dev.js +28 -0
  107. package/dist/vite/index.d.ts +1 -0
  108. package/dist/vite/index.js +1 -0
  109. package/dist/vitest-jsdom.test.d.ts +8 -0
  110. package/package.json +28 -16
  111. package/src/admin-shell/chrome/admin-menu-drawer.tsx +19 -5
  112. package/src/admin-shell/chrome/dashboard.test.tsx +199 -0
  113. package/src/admin-shell/chrome/dashboard.tsx +43 -9
  114. package/src/admin-shell/collections/api.tsx +2 -2
  115. package/src/admin-shell/collections/create.tsx +6 -2
  116. package/src/admin-shell/collections/edit.tsx +6 -2
  117. package/src/admin-shell/collections/history.module.css +1 -1
  118. package/src/admin-shell/collections/history.tsx +162 -442
  119. package/src/admin-shell/collections/reindex-button.tsx +4 -2
  120. package/src/admin-shell/collections/restore-version-modal.tsx +19 -25
  121. package/src/admin-shell/collections/version-history.tsx +349 -0
  122. package/src/admin-shell/singletons/history.test.tsx +407 -0
  123. package/src/admin-shell/singletons/history.tsx +185 -0
  124. package/src/admin-shell/singletons/preview-link.tsx +75 -0
  125. package/src/admin-shell/singletons/view-menu.tsx +136 -0
  126. package/src/admin-shell/singletons/view.test.tsx +400 -0
  127. package/src/admin-shell/singletons/view.tsx +351 -0
  128. package/src/integrations/analytics-agent.tsx +91 -0
  129. package/src/integrations/analytics-events.test.node.ts +245 -0
  130. package/src/integrations/analytics-events.ts +224 -0
  131. package/src/integrations/api-utils.ts +34 -9
  132. package/src/integrations/serve-analytics-agent.test.node.ts +20 -0
  133. package/src/integrations/serve-analytics-agent.ts +19 -0
  134. package/src/routes/analytics-range.ts +55 -0
  135. package/src/routes/create-admin-analytics-route.test.node.ts +46 -0
  136. package/src/routes/create-admin-analytics-route.tsx +109 -0
  137. package/src/routes/create-admin-dashboard-route.test.node.ts +70 -0
  138. package/src/routes/create-admin-dashboard-route.tsx +10 -2
  139. package/src/routes/create-admin-layout-route.tsx +15 -8
  140. package/src/routes/create-collection-api-route.tsx +4 -4
  141. package/src/routes/create-collection-create-route.tsx +5 -8
  142. package/src/routes/create-collection-edit-route.tsx +5 -4
  143. package/src/routes/create-collection-history-route.tsx +5 -8
  144. package/src/routes/create-collection-list-route.tsx +5 -8
  145. package/src/routes/create-singleton-history-route.test.node.ts +148 -0
  146. package/src/routes/create-singleton-history-route.tsx +116 -0
  147. package/src/routes/create-singleton-route.test.node.ts +172 -0
  148. package/src/routes/create-singleton-route.tsx +176 -0
  149. package/src/routes/get-multi-collection-definition.ts +16 -0
  150. package/src/routes/get-singleton-definition.ts +16 -0
  151. package/src/routes/index.ts +3 -0
  152. package/src/server-fns/actors.ts +43 -0
  153. package/src/server-fns/admin-document-presentation.ts +78 -0
  154. package/src/server-fns/analytics/index.ts +23 -0
  155. package/src/server-fns/analytics/maintenance.ts +54 -0
  156. package/src/server-fns/analytics/queries.ts +86 -0
  157. package/src/server-fns/collections/actors.ts +1 -52
  158. package/src/server-fns/collections/get.ts +10 -49
  159. package/src/server-fns/collections/reorder.ts +1 -1
  160. package/src/server-fns/collections/scheduled-publication.ts +7 -1
  161. package/src/server-fns/collections/upload-singleton.test.node.ts +164 -0
  162. package/src/server-fns/collections/upload.ts +7 -3
  163. package/src/server-fns/collections/utils.ts +1 -9
  164. package/src/server-fns/serialise.ts +17 -0
  165. package/src/server-fns/singleton-document-read.ts +32 -0
  166. package/src/server-fns/singletons/cancel-scheduled-publish.ts +19 -0
  167. package/src/server-fns/singletons/change-status.ts +19 -0
  168. package/src/server-fns/singletons/confirm-scheduled-publish.ts +23 -0
  169. package/src/server-fns/singletons/copy-to-locale.ts +32 -0
  170. package/src/server-fns/singletons/find-by-version.ts +23 -0
  171. package/src/server-fns/singletons/get-scheduled-publish.ts +19 -0
  172. package/src/server-fns/singletons/get.ts +32 -0
  173. package/src/server-fns/singletons/history.ts +41 -0
  174. package/src/server-fns/singletons/index.ts +20 -0
  175. package/src/server-fns/singletons/restore-version.ts +19 -0
  176. package/src/server-fns/singletons/schedule-publish.ts +24 -0
  177. package/src/server-fns/singletons/singletons.test.node.ts +298 -0
  178. package/src/server-fns/singletons/unpublish.ts +19 -0
  179. package/src/server-fns/singletons/update.ts +31 -0
  180. package/src/vite/analytics-dev.test.node.ts +126 -0
  181. package/src/vite/analytics-dev.ts +56 -0
  182. package/src/vite/index.ts +1 -0
  183. package/src/vitest-jsdom.test.tsx +27 -0
@@ -0,0 +1,116 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect } from "react";
4
+ import { useTranslation } from "@byline/i18n/react";
5
+ import { Button, HistoryIcon, IconButton, Label, Select } from "@byline/ui/react";
6
+ import clsx from "clsx";
7
+ import { getAdminRoutePath } from "../../routes/admin-path.js";
8
+ import { useNavigate } from "../chrome/loose-router.js";
9
+ import view_menu_module from "../collections/view-menu.module.js";
10
+ import { SingletonPreviewLink } from "./preview-link.js";
11
+ function SingletonViewMenu({ singleton, activeView, locale, contentLocales, defaultContentLocale, adminConfig, doc }) {
12
+ const navigate = useNavigate();
13
+ const { t } = useTranslation('byline-admin');
14
+ useEffect(()=>{
15
+ if ('edit' === activeView && 'all' === locale) navigate({
16
+ to: getAdminRoutePath('singletons', '$singleton'),
17
+ params: {
18
+ singleton
19
+ },
20
+ search: {
21
+ locale: defaultContentLocale
22
+ },
23
+ replace: true
24
+ });
25
+ }, [
26
+ activeView,
27
+ locale,
28
+ singleton,
29
+ defaultContentLocale,
30
+ navigate
31
+ ]);
32
+ const handleLocaleChange = (value)=>{
33
+ if (null == value) return;
34
+ navigate({
35
+ to: 'history' === activeView ? getAdminRoutePath('singletons', '$singleton', 'history') : getAdminRoutePath('singletons', '$singleton'),
36
+ params: {
37
+ singleton
38
+ },
39
+ search: {
40
+ locale: value
41
+ }
42
+ });
43
+ };
44
+ return /*#__PURE__*/ jsxs("div", {
45
+ className: clsx('byline-view-menu', view_menu_module.root),
46
+ children: [
47
+ /*#__PURE__*/ jsx(Label, {
48
+ className: clsx('muted byline-view-menu-label', view_menu_module.label),
49
+ id: "contentLocaleLabel",
50
+ htmlFor: "contentLocale",
51
+ label: t('collections.viewMenu.contentLocaleLabel')
52
+ }),
53
+ /*#__PURE__*/ jsx(Select, {
54
+ name: "contentLocale",
55
+ id: "contentLocale",
56
+ className: clsx('byline-view-menu-locale-select', view_menu_module.localeSelect),
57
+ size: "xs",
58
+ variant: "outlined",
59
+ value: locale ?? defaultContentLocale,
60
+ items: [
61
+ ...'history' === activeView ? [
62
+ {
63
+ value: 'all',
64
+ label: t('collections.viewMenu.localeAll')
65
+ }
66
+ ] : [],
67
+ ...contentLocales.map((entry)=>({
68
+ value: entry.code,
69
+ label: entry.label
70
+ }))
71
+ ],
72
+ onValueChange: handleLocaleChange
73
+ }),
74
+ adminConfig?.preview != null && null != doc && /*#__PURE__*/ jsx(SingletonPreviewLink, {
75
+ doc: doc,
76
+ adminConfig: adminConfig,
77
+ locale: locale,
78
+ className: clsx('byline-view-menu-icon-button', view_menu_module.iconButton)
79
+ }),
80
+ /*#__PURE__*/ jsx(IconButton, {
81
+ className: clsx('byline-view-menu-icon-button', view_menu_module.iconButton),
82
+ size: "xs",
83
+ variant: 'history' === activeView ? 'filled' : 'text',
84
+ "aria-label": t('collections.breadcrumbs.history'),
85
+ onClick: ()=>navigate({
86
+ to: getAdminRoutePath('singletons', '$singleton', 'history'),
87
+ params: {
88
+ singleton
89
+ },
90
+ search: locale ? {
91
+ locale
92
+ } : {}
93
+ }),
94
+ children: /*#__PURE__*/ jsx(HistoryIcon, {
95
+ className: clsx('byline-view-menu-icon-button-icon', view_menu_module.iconButtonIcon)
96
+ })
97
+ }),
98
+ /*#__PURE__*/ jsx(Button, {
99
+ size: "xs",
100
+ variant: 'edit' === activeView ? 'filled' : 'outlined',
101
+ className: clsx('byline-view-menu-button', view_menu_module.button),
102
+ onClick: ()=>navigate({
103
+ to: getAdminRoutePath('singletons', '$singleton'),
104
+ params: {
105
+ singleton
106
+ },
107
+ search: locale ? {
108
+ locale
109
+ } : {}
110
+ }),
111
+ children: t('common.actions.edit')
112
+ })
113
+ ]
114
+ });
115
+ }
116
+ export { SingletonViewMenu };
@@ -0,0 +1,18 @@
1
+ import type { SingletonAdminConfig, SingletonDefinition } from '@byline/core';
2
+ import type { ContentLocaleOption } from '../collections/view-menu.js';
3
+ type SingletonDocument = Record<string, any> & {
4
+ id: string;
5
+ status: string;
6
+ fields: Record<string, any>;
7
+ };
8
+ export declare function SingletonView({ singletonDefinition, adminConfig, document, initialData, locale, contentLocales, defaultContentLocale, }: {
9
+ singletonDefinition: SingletonDefinition;
10
+ adminConfig?: SingletonAdminConfig;
11
+ document: SingletonDocument | null;
12
+ /** Schema defaults resolved by the server-side route loader for an empty slot. */
13
+ initialData?: Record<string, any>;
14
+ locale?: string;
15
+ contentLocales: ReadonlyArray<ContentLocaleOption>;
16
+ defaultContentLocale: string;
17
+ }): import("react").JSX.Element;
18
+ export {};
@@ -0,0 +1,259 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useRouter } from "@tanstack/react-router";
4
+ import { FormRenderer } from "@byline/admin/react";
5
+ import { ErrorCodes, getDefaultStatus, getWorkflow, getWorkflowStatuses, validateStatusTransition } from "@byline/core";
6
+ import { useTranslation } from "@byline/i18n/react";
7
+ import { Container, Section, useToastManager } from "@byline/ui/react";
8
+ import { getAdminRoutePath } from "../../routes/admin-path.js";
9
+ import { cancelSingletonScheduledPublish, changeSingletonStatus, confirmSingletonScheduledPublish, copySingletonToLocale, scheduleSingletonPublish, unpublishSingleton, updateSingleton } from "../../server-fns/singletons/index.js";
10
+ import { useNavigate } from "../chrome/loose-router.js";
11
+ import { useTanStackNavigationGuard } from "../collections/tanstack-navigation-guard.js";
12
+ import { SingletonViewMenu } from "./view-menu.js";
13
+ function errorCode(error) {
14
+ return 'string' == typeof error?.code ? error.code : null;
15
+ }
16
+ function SingletonView({ singletonDefinition, adminConfig, document, initialData, locale, contentLocales, defaultContentLocale }) {
17
+ const router = useRouter();
18
+ const navigate = useNavigate();
19
+ const toastManager = useToastManager();
20
+ const { t } = useTranslation('byline-admin');
21
+ const { path, label, fields } = singletonDefinition;
22
+ const workflowStatuses = getWorkflowStatuses(singletonDefinition);
23
+ const currentStatus = document?.status ?? getDefaultStatus(singletonDefinition);
24
+ const currentIndex = workflowStatuses.findIndex((status)=>status.name === currentStatus);
25
+ const nextStatus = -1 !== currentIndex && currentIndex < workflowStatuses.length - 1 ? workflowStatuses[currentIndex + 1] : void 0;
26
+ const publishedVersion = document?._publishedVersion ?? null;
27
+ const restoreWarnings = document?._restoreWarnings;
28
+ const scheduledPublicationEnabled = document?._scheduledPublicationEnabled === true;
29
+ const canSchedulePublication = document?._canSchedulePublication === true;
30
+ const scheduledPublication = document?._scheduledPublish ?? null;
31
+ const publishTransitionValid = null != document && 'published' !== currentStatus && workflowStatuses.length > 1 && validateStatusTransition(getWorkflow(singletonDefinition), currentStatus, 'published').valid;
32
+ const canArmSchedule = scheduledPublicationEnabled && canSchedulePublication && publishTransitionValid;
33
+ const reload = async ()=>{
34
+ await router.invalidate();
35
+ };
36
+ const toast = (title, description, intent)=>{
37
+ toastManager.add({
38
+ title,
39
+ description,
40
+ data: {
41
+ intent,
42
+ iconType: intent,
43
+ icon: true,
44
+ close: true
45
+ }
46
+ });
47
+ };
48
+ const notifyScheduleSuspended = ()=>{
49
+ if (scheduledPublication?.state !== 'armed') return;
50
+ toast(t('scheduledPublication.toast.suspendedTitle'), t("scheduledPublication.toast.suspendedDescription"), 'warning');
51
+ };
52
+ const handleSubmit = async ({ data })=>{
53
+ try {
54
+ await updateSingleton({
55
+ data: {
56
+ singleton: path,
57
+ data,
58
+ locale: locale ?? defaultContentLocale,
59
+ expectedVersionId: document?.versionId
60
+ }
61
+ });
62
+ if (null != document) notifyScheduleSuspended();
63
+ toast(t('singletons.edit.updateTitle', {
64
+ label
65
+ }), t("singletons.edit.updatedDescription", {
66
+ label: label.toLowerCase()
67
+ }), 'success');
68
+ await reload();
69
+ } catch (err) {
70
+ const code = errorCode(err);
71
+ const description = code === ErrorCodes.CONFLICT ? t("singletons.edit.conflictDescription") : code === ErrorCodes.NOT_FOUND ? t("singletons.edit.notConfiguredDescription") : t("singletons.edit.updateFailedDescription", {
72
+ label: label.toLowerCase()
73
+ });
74
+ toast(t('singletons.edit.updateTitle', {
75
+ label
76
+ }), description, 'danger');
77
+ throw err;
78
+ }
79
+ };
80
+ const handleStatusChange = async (status)=>{
81
+ try {
82
+ await changeSingletonStatus({
83
+ data: {
84
+ singleton: path,
85
+ status
86
+ }
87
+ });
88
+ toast(t('collections.edit.statusUpdateTitle', {
89
+ label
90
+ }), t("collections.edit.statusChangedDescription", {
91
+ status
92
+ }), 'success');
93
+ await reload();
94
+ } catch (err) {
95
+ toast(t('collections.edit.statusUpdateTitle', {
96
+ label
97
+ }), t("collections.edit.statusChangeFailedDescription", {
98
+ message: err.message
99
+ }), 'danger');
100
+ throw err;
101
+ }
102
+ };
103
+ const handleUnpublish = async ()=>{
104
+ try {
105
+ await unpublishSingleton({
106
+ data: {
107
+ singleton: path
108
+ }
109
+ });
110
+ toast(t('collections.edit.unpublishTitle', {
111
+ label
112
+ }), t("collections.edit.unpublishedDescription"), 'success');
113
+ await reload();
114
+ } catch (err) {
115
+ toast(t('collections.edit.unpublishTitle', {
116
+ label
117
+ }), t("collections.edit.unpublishFailedDescription", {
118
+ message: err.message
119
+ }), 'danger');
120
+ throw err;
121
+ }
122
+ };
123
+ const handleCopyToLocale = async ({ targetLocale, overwrite })=>{
124
+ try {
125
+ await copySingletonToLocale({
126
+ data: {
127
+ singleton: path,
128
+ sourceLocale: locale ?? defaultContentLocale,
129
+ targetLocale,
130
+ overwrite
131
+ }
132
+ });
133
+ const sourceLocale = locale ?? defaultContentLocale;
134
+ const sourceLabel = contentLocales.find((entry)=>entry.code === sourceLocale)?.label ?? sourceLocale;
135
+ const targetLabel = contentLocales.find((entry)=>entry.code === targetLocale)?.label ?? targetLocale;
136
+ toast(t('collections.edit.copyToLocaleTitle', {
137
+ label
138
+ }), t('collections.edit.copiedSuccessMessage', {
139
+ source: sourceLabel,
140
+ target: targetLabel
141
+ }), 'success');
142
+ notifyScheduleSuspended();
143
+ navigate({
144
+ to: getAdminRoutePath('singletons', '$singleton'),
145
+ params: {
146
+ singleton: path
147
+ },
148
+ search: {
149
+ locale: targetLocale
150
+ }
151
+ });
152
+ } catch (err) {
153
+ toast(t('collections.edit.copyToLocaleTitle', {
154
+ label
155
+ }), t("collections.edit.copyFailedDescription", {
156
+ message: err.message
157
+ }), 'danger');
158
+ throw err;
159
+ }
160
+ };
161
+ const handleSchedulePublication = async ({ publishAt })=>{
162
+ try {
163
+ await scheduleSingletonPublish({
164
+ data: {
165
+ singleton: path,
166
+ publishAt,
167
+ expectedVersionId: String(document?.versionId)
168
+ }
169
+ });
170
+ toast(t('scheduledPublication.toast.scheduledTitle'), t("scheduledPublication.toast.scheduledDescription"), 'success');
171
+ await reload();
172
+ } catch (err) {
173
+ toast(t('scheduledPublication.toast.failedTitle'), err.message, 'danger');
174
+ throw err;
175
+ }
176
+ };
177
+ const handleConfirmScheduledPublication = async ()=>{
178
+ try {
179
+ await confirmSingletonScheduledPublish({
180
+ data: {
181
+ singleton: path,
182
+ expectedVersionId: String(document?.versionId)
183
+ }
184
+ });
185
+ toast(t('scheduledPublication.toast.confirmedTitle'), t("scheduledPublication.toast.confirmedDescription"), 'success');
186
+ await reload();
187
+ } catch (err) {
188
+ toast(t('scheduledPublication.toast.failedTitle'), err.message, 'danger');
189
+ throw err;
190
+ }
191
+ };
192
+ const handleCancelScheduledPublication = async ()=>{
193
+ try {
194
+ await cancelSingletonScheduledPublish({
195
+ data: {
196
+ singleton: path
197
+ }
198
+ });
199
+ toast(t('scheduledPublication.toast.cancelledTitle'), t("scheduledPublication.toast.cancelledDescription"), 'success');
200
+ await reload();
201
+ } catch (err) {
202
+ toast(t('scheduledPublication.toast.failedTitle'), err.message, 'danger');
203
+ throw err;
204
+ }
205
+ };
206
+ return /*#__PURE__*/ jsx(Section, {
207
+ children: /*#__PURE__*/ jsx(Container, {
208
+ children: /*#__PURE__*/ jsx(FormRenderer, {
209
+ mode: null == document ? 'create' : 'edit',
210
+ fields: fields,
211
+ onSubmit: handleSubmit,
212
+ initialData: document ?? (initialData ? {
213
+ fields: initialData
214
+ } : void 0),
215
+ adminConfig: adminConfig,
216
+ heading: label,
217
+ showPath: false,
218
+ initialLocale: locale,
219
+ defaultLocale: defaultContentLocale,
220
+ onLocaleChange: null == document ? void 0 : (nextLocale)=>navigate({
221
+ to: getAdminRoutePath('singletons', '$singleton'),
222
+ params: {
223
+ singleton: path
224
+ },
225
+ search: {
226
+ locale: nextLocale
227
+ }
228
+ }),
229
+ useNavigationGuard: useTanStackNavigationGuard,
230
+ headerSlot: null == document ? void 0 : /*#__PURE__*/ jsx(SingletonViewMenu, {
231
+ singleton: path,
232
+ activeView: "edit",
233
+ locale: locale,
234
+ contentLocales: contentLocales,
235
+ defaultContentLocale: defaultContentLocale,
236
+ adminConfig: adminConfig,
237
+ doc: document
238
+ }),
239
+ onStatusChange: null == document ? void 0 : handleStatusChange,
240
+ onUnpublish: null != document && publishedVersion ? handleUnpublish : void 0,
241
+ scheduledPublication: null == document ? null : scheduledPublication,
242
+ onSchedulePublication: canArmSchedule ? handleSchedulePublication : void 0,
243
+ onConfirmScheduledPublication: canArmSchedule && scheduledPublication?.state === 'needs_reconfirm' ? handleConfirmScheduledPublication : void 0,
244
+ onCancelScheduledPublication: null != document && null != scheduledPublication ? handleCancelScheduledPublication : void 0,
245
+ onCopyToLocale: null == document ? void 0 : handleCopyToLocale,
246
+ contentLocales: null == document ? void 0 : contentLocales,
247
+ publishedVersion: null == document ? null : publishedVersion,
248
+ restoreWarnings: restoreWarnings,
249
+ nextStatus: null == document ? void 0 : nextStatus,
250
+ workflowStatuses: null == document ? void 0 : workflowStatuses,
251
+ onCancel: ()=>navigate({
252
+ to: getAdminRoutePath()
253
+ }),
254
+ collectionPath: path
255
+ })
256
+ })
257
+ });
258
+ }
259
+ export { SingletonView };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type React from 'react';
9
+ export interface AnalyticsAgentProps {
10
+ /** Application-owned same-origin route that receives browser events. */
11
+ endpoint: string;
12
+ src?: string;
13
+ cdnHosts?: readonly string[];
14
+ downloadExtensions?: readonly string[];
15
+ ignoredPathPrefixes?: readonly string[];
16
+ countSearchChanges?: boolean;
17
+ countHashChanges?: boolean;
18
+ }
19
+ /** Render the standalone script and bridge only committed router locations into it. */
20
+ export declare function AnalyticsAgent({ endpoint, src, cdnHosts, downloadExtensions, ignoredPathPrefixes, countSearchChanges, countHashChanges, }: AnalyticsAgentProps): React.JSX.Element;
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from "react";
4
+ import { useRouterState } from "@tanstack/react-router";
5
+ import { ANALYTICS_NAVIGATION_EVENT, ANALYTICS_STOP_EVENT } from "@byline/analytics-agent";
6
+ let pendingStopTimer;
7
+ function AnalyticsAgent({ endpoint, src = '/b.js', cdnHosts, downloadExtensions, ignoredPathPrefixes, countSearchChanges = false, countHashChanges = false }) {
8
+ return /*#__PURE__*/ jsxs(Fragment, {
9
+ children: [
10
+ /*#__PURE__*/ jsx("script", {
11
+ defer: true,
12
+ src: src,
13
+ "data-endpoint": endpoint,
14
+ "data-cdn-hosts": boundedAttribute(cdnHosts),
15
+ "data-download-extensions": boundedAttribute(downloadExtensions),
16
+ "data-ignore-prefixes": boundedAttribute(ignoredPathPrefixes),
17
+ "data-count-search": countSearchChanges ? 'true' : void 0,
18
+ "data-count-hash": countHashChanges ? 'true' : void 0
19
+ }),
20
+ /*#__PURE__*/ jsx(AnalyticsNavigationBridge, {})
21
+ ]
22
+ });
23
+ }
24
+ function AnalyticsNavigationBridge() {
25
+ const href = useRouterState({
26
+ select: (state)=>state.location.href
27
+ });
28
+ const previousHref = useRef(void 0);
29
+ useEffect(()=>{
30
+ const previous = previousHref.current;
31
+ previousHref.current = href;
32
+ if (null == previous || previous === href) return;
33
+ document.dispatchEvent(new CustomEvent(ANALYTICS_NAVIGATION_EVENT, {
34
+ detail: href
35
+ }));
36
+ }, [
37
+ href
38
+ ]);
39
+ useEffect(()=>{
40
+ if (void 0 !== pendingStopTimer) {
41
+ window.clearTimeout(pendingStopTimer);
42
+ pendingStopTimer = void 0;
43
+ }
44
+ return ()=>{
45
+ pendingStopTimer = window.setTimeout(()=>{
46
+ pendingStopTimer = void 0;
47
+ document.dispatchEvent(new Event(ANALYTICS_STOP_EVENT));
48
+ }, 0);
49
+ };
50
+ }, []);
51
+ return null;
52
+ }
53
+ function boundedAttribute(values) {
54
+ if (null == values || 0 === values.length) return;
55
+ return values.slice(0, 64).join(',').slice(0, 2048);
56
+ }
57
+ export { AnalyticsAgent };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { type Analytics } from '@byline/analytics';
9
+ import { type BylineLogger } from '@byline/core';
10
+ export interface AnalyticsEventHandlerOptions {
11
+ /** Host-owned resolver for deployment-specific, request-scoped network facts. */
12
+ resolveRequestContext: AnalyticsRequestContextResolver;
13
+ /** Test/embedding seam; production resolves the registered installation runtime. */
14
+ analytics?: () => Pick<Analytics, 'ingest'>;
15
+ /** Test/embedding seam; production resolves the initialized application logger. */
16
+ logger?: () => Pick<BylineLogger, 'error'>;
17
+ }
18
+ export interface AnalyticsRequestContext {
19
+ clientIp?: string | null;
20
+ country?: string | null;
21
+ }
22
+ export type AnalyticsRequestContextResolver = (request: Request) => AnalyticsRequestContext | Promise<AnalyticsRequestContext>;
23
+ export interface LocalAnalyticsRequestContextOptions {
24
+ /** Resolve the direct peer address from the host runtime, without forwarded headers. */
25
+ resolveClientIp: (request: Request) => string | null | undefined | Promise<string | null | undefined>;
26
+ /** Explicit development-only identity for runtimes that expose no peer address. */
27
+ developmentFallbackClientIp?: string;
28
+ }
29
+ export interface TrustedAnalyticsHeadersOptions {
30
+ /** Header overwritten by the trusted reverse proxy with the normalized client address. */
31
+ clientIpHeader: string;
32
+ /** Optional header overwritten by the trusted edge with an ISO country code. */
33
+ countryHeader?: string;
34
+ /** Explicit context or resolver used only when a trusted header is absent. */
35
+ fallback?: AnalyticsRequestContext | AnalyticsRequestContextResolver;
36
+ }
37
+ /**
38
+ * Resolve an identity for a server reached directly through a loopback URL.
39
+ * Both the URL and the direct peer address must be loopback. This is safe for
40
+ * a directly reached local server. A same-host reverse proxy also appears as
41
+ * a loopback peer, however, so do not expose this fallback through a public
42
+ * proxy where a client can select the Host header.
43
+ */
44
+ export declare function localAnalyticsRequestContext(options: LocalAnalyticsRequestContextOptions): AnalyticsRequestContextResolver;
45
+ /**
46
+ * Resolve network facts from headers owned and overwritten by a trusted proxy.
47
+ * This helper does not establish that trust: the deployment must prevent a
48
+ * client from selecting the configured header values or bypassing the proxy.
49
+ */
50
+ export declare function trustedAnalyticsHeaders(options: TrustedAnalyticsHeadersOptions): AnalyticsRequestContextResolver;
51
+ export declare function createAnalyticsEventHandler(options: AnalyticsEventHandlerOptions): (request: Request) => Promise<Response>;
@@ -0,0 +1,129 @@
1
+ import { ANALYTICS_MAX_BODY_BYTES, getAnalytics } from "@byline/analytics";
2
+ import { getLogger } from "@byline/core";
3
+ function localAnalyticsRequestContext(options) {
4
+ const developmentFallbackClientIp = null == options.developmentFallbackClientIp ? void 0 : validateFact(options.developmentFallbackClientIp, 'developmentFallbackClientIp', 255);
5
+ return async (request)=>{
6
+ const clientIp = await options.resolveClientIp(request);
7
+ if (isLoopbackHostname(new URL(request.url).hostname) && isLoopbackAddress(clientIp)) return {
8
+ clientIp: validateFact(clientIp, 'resolved clientIp', 255)
9
+ };
10
+ return null == developmentFallbackClientIp ? {} : {
11
+ clientIp: developmentFallbackClientIp
12
+ };
13
+ };
14
+ }
15
+ function trustedAnalyticsHeaders(options) {
16
+ const clientIpHeader = validateHeaderName(options.clientIpHeader, 'clientIpHeader');
17
+ const countryHeader = null == options.countryHeader ? void 0 : validateHeaderName(options.countryHeader, 'countryHeader');
18
+ const fallback = 'function' == typeof options.fallback ? options.fallback : validateFallback(options.fallback);
19
+ return async (request)=>{
20
+ const fallbackContext = 'function' == typeof fallback ? validateFallback(await fallback(request)) : fallback;
21
+ return {
22
+ clientIp: request.headers.get(clientIpHeader)?.trim() || fallbackContext?.clientIp,
23
+ country: (null == countryHeader ? null : request.headers.get(countryHeader)?.trim()) || fallbackContext?.country
24
+ };
25
+ };
26
+ }
27
+ function createAnalyticsEventHandler(options) {
28
+ const resolveAnalytics = options.analytics ?? getAnalytics;
29
+ return async (request)=>{
30
+ const body = await readBoundedBody(request, ANALYTICS_MAX_BODY_BYTES);
31
+ try {
32
+ const requestContext = await options.resolveRequestContext(request);
33
+ const result = await resolveAnalytics().ingest({
34
+ method: request.method,
35
+ body: body ?? new Uint8Array(ANALYTICS_MAX_BODY_BYTES + 1),
36
+ origin: request.headers.get('origin'),
37
+ referer: request.headers.get('referer'),
38
+ userAgent: request.headers.get('user-agent'),
39
+ secPurpose: request.headers.get('sec-purpose'),
40
+ xPurpose: request.headers.get('x-purpose'),
41
+ clientIp: requestContext.clientIp,
42
+ country: requestContext.country
43
+ });
44
+ return analyticsResponse(result.status);
45
+ } catch (error) {
46
+ const logger = options.logger?.() ?? getLogger();
47
+ logger.error({
48
+ errorCode: safeErrorCode(error)
49
+ }, '[analytics] event persistence failed');
50
+ return analyticsResponse(503);
51
+ }
52
+ };
53
+ }
54
+ function safeErrorCode(error) {
55
+ if (null == error || 'object' != typeof error || !('code' in error)) return 'UNKNOWN';
56
+ const code = error.code;
57
+ return 'string' == typeof code && /^[a-z0-9_-]{1,64}$/iu.test(code) ? code : 'UNKNOWN';
58
+ }
59
+ function analyticsResponse(status) {
60
+ return new Response(null, {
61
+ status,
62
+ headers: {
63
+ 'cache-control': 'no-store'
64
+ }
65
+ });
66
+ }
67
+ async function readBoundedBody(request, limit) {
68
+ const declaredLength = Number(request.headers.get('content-length'));
69
+ if (Number.isFinite(declaredLength) && declaredLength > limit) return null;
70
+ if (null == request.body) return new Uint8Array();
71
+ const reader = request.body.getReader();
72
+ const chunks = [];
73
+ let length = 0;
74
+ try {
75
+ while(true){
76
+ const { done, value } = await reader.read();
77
+ if (done) break;
78
+ length += value.byteLength;
79
+ if (length > limit) {
80
+ await reader.cancel();
81
+ return null;
82
+ }
83
+ chunks.push(value);
84
+ }
85
+ } finally{
86
+ reader.releaseLock();
87
+ }
88
+ const body = new Uint8Array(length);
89
+ let offset = 0;
90
+ for (const chunk of chunks){
91
+ body.set(chunk, offset);
92
+ offset += chunk.byteLength;
93
+ }
94
+ return body;
95
+ }
96
+ function validateHeaderName(value, label) {
97
+ const normalized = value.trim().toLowerCase();
98
+ if (!/^[!#$%&'*+.^_`|~0-9a-z-]+$/u.test(normalized)) throw new Error(`analytics ${label} must be a valid HTTP header name`);
99
+ return normalized;
100
+ }
101
+ function validateFallback(value) {
102
+ if (null == value) return;
103
+ return {
104
+ ...null == value.clientIp ? {} : {
105
+ clientIp: validateFact(value.clientIp, 'fallback.clientIp', 255)
106
+ },
107
+ ...null == value.country ? {} : {
108
+ country: validateFact(value.country, 'fallback.country', 16)
109
+ }
110
+ };
111
+ }
112
+ function validateFact(value, label, maxLength) {
113
+ const normalized = value.trim();
114
+ if (0 === normalized.length || normalized.length > maxLength) throw new Error(`analytics ${label} must contain 1 to ${maxLength} characters`);
115
+ return normalized;
116
+ }
117
+ function isLoopbackHostname(value) {
118
+ const normalized = stripIpv6Brackets(value.toLowerCase());
119
+ return 'localhost' === normalized || normalized.endsWith('.localhost') || isLoopbackAddress(normalized);
120
+ }
121
+ function isLoopbackAddress(value) {
122
+ if (null == value) return false;
123
+ const normalized = stripIpv6Brackets(value.trim().toLowerCase());
124
+ return '::1' === normalized || /^127(?:\.\d{1,3}){3}$/u.test(normalized) || /^::ffff:127(?:\.\d{1,3}){3}$/u.test(normalized);
125
+ }
126
+ function stripIpv6Brackets(value) {
127
+ return value.startsWith('[') && value.endsWith(']') ? value.slice(1, -1) : value;
128
+ }
129
+ export { createAnalyticsEventHandler, localAnalyticsRequestContext, trustedAnalyticsHeaders };