@escalated-dev/escalated 0.11.1 → 0.11.3

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,85 +1,89 @@
1
1
  {
2
- "name": "@escalated-dev/escalated",
3
- "version": "0.11.1",
4
- "description": "Vue 3 + Inertia.js UI components for Escalated — the embeddable support ticket system",
5
- "author": "Escalated Dev <hello@escalated.dev>",
6
- "license": "MIT",
7
- "homepage": "https://github.com/escalated-dev/escalated",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/escalated-dev/escalated.git"
11
- },
12
- "keywords": [
13
- "vue",
14
- "inertia",
15
- "support",
16
- "tickets",
17
- "helpdesk",
18
- "escalated"
19
- ],
20
- "type": "module",
21
- "exports": {
22
- "./components/*": "./src/components/*",
23
- "./pages/*": "./src/pages/*",
24
- "./locales": "./src/locales/index.js",
25
- "./locales/*": "./src/locales/*",
26
- ".": "./src/index.js"
27
- },
28
- "files": [
29
- "src"
30
- ],
31
- "peerDependencies": {
32
- "@inertiajs/vue3": "^1.0.0 || ^2.0.0 || ^3.0.0",
33
- "vue": "^3.3.0"
34
- },
35
- "dependencies": {
36
- "@escalated-dev/locale": "^0.1.8"
37
- },
38
- "scripts": {
39
- "test": "vitest run",
40
- "lint": "eslint src/ --max-warnings 0",
41
- "lint:fix": "eslint src/ --fix",
42
- "format": "prettier --write src/",
43
- "format:check": "prettier --check src/",
44
- "prepare": "husky",
45
- "storybook": "storybook dev -p 6006",
46
- "build-storybook": "storybook build",
47
- "screenshot": "npx playwright test --config=playwright-screenshots.config.js",
48
- "demo:serve": "node scripts/demo-server.js",
49
- "demo:record": "node scripts/generate-demo.js",
50
- "demo": "concurrently -k -s second \"npm run demo:serve\" \"wait-on http://localhost:3000 && npm run demo:record\""
51
- },
52
- "lint-staged": {
53
- "*.{js,vue}": [
54
- "eslint --fix",
55
- "prettier --write"
56
- ]
57
- },
58
- "devDependencies": {
59
- "@eslint/js": "^10.0.1",
60
- "@inertiajs/vue3": "^3.7.0",
61
- "@playwright/test": "^1.62.1",
62
- "@storybook/addon-a11y": "^10.6.0",
63
- "@storybook/addon-docs": "^10.6.0",
64
- "@storybook/vue3-vite": "^10.6.0",
65
- "@tailwindcss/postcss": "^4.3.3",
66
- "@vitejs/plugin-vue": "^6.0.8",
67
- "@vue/test-utils": "^2.5.0",
68
- "autoprefixer": "^10.5.4",
69
- "concurrently": "^10.0.5",
70
- "eslint": "^10.9.1",
71
- "eslint-plugin-storybook": "^10.5.10",
72
- "eslint-plugin-vue": "^10.10.0",
73
- "happy-dom": "^20.14.0",
74
- "husky": "^9.1.7",
75
- "lint-staged": "^17.4.1",
76
- "playwright": "^1.62.1",
77
- "postcss": "^8.5.26",
78
- "prettier": "^3.9.6",
79
- "storybook": "^10.5.10",
80
- "tailwindcss": "^4.3.3",
81
- "vitest": "^5.0.0",
82
- "vue": "^3.5.42",
83
- "wait-on": "^9.1.0"
84
- }
2
+ "name": "@escalated-dev/escalated",
3
+ "version": "0.11.3",
4
+ "description": "Vue 3 + Inertia.js UI components for Escalated — the embeddable support ticket system",
5
+ "author": "Escalated Dev <hello@escalated.dev>",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/escalated-dev/escalated",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/escalated-dev/escalated.git"
11
+ },
12
+ "keywords": [
13
+ "vue",
14
+ "inertia",
15
+ "support",
16
+ "tickets",
17
+ "helpdesk",
18
+ "escalated"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ "./components/*": "./src/components/*",
23
+ "./pages/*": "./src/pages/*",
24
+ "./locales": "./src/locales/index.js",
25
+ "./locales/*": "./src/locales/*",
26
+ ".": "./src/index.js",
27
+ "./pages.json": "./pages.json"
28
+ },
29
+ "files": [
30
+ "src",
31
+ "pages.json"
32
+ ],
33
+ "peerDependencies": {
34
+ "@inertiajs/vue3": "^1.0.0 || ^2.0.0 || ^3.0.0",
35
+ "vue": "^3.3.0"
36
+ },
37
+ "dependencies": {
38
+ "@escalated-dev/locale": "^0.1.8"
39
+ },
40
+ "scripts": {
41
+ "test": "vitest run",
42
+ "lint": "eslint src/ --max-warnings 0",
43
+ "lint:fix": "eslint src/ --fix",
44
+ "format": "prettier --write src/",
45
+ "format:check": "prettier --check src/",
46
+ "prepare": "husky",
47
+ "storybook": "storybook dev -p 6006",
48
+ "build-storybook": "storybook build",
49
+ "screenshot": "npx playwright test --config=playwright-screenshots.config.js",
50
+ "demo:serve": "node scripts/demo-server.js",
51
+ "demo:record": "node scripts/generate-demo.js",
52
+ "demo": "concurrently -k -s second \"npm run demo:serve\" \"wait-on http://localhost:3000 && npm run demo:record\"",
53
+ "pages:manifest": "node scripts/generate-page-manifest.js",
54
+ "pages:check": "node scripts/generate-page-manifest.js --check"
55
+ },
56
+ "lint-staged": {
57
+ "*.{js,vue}": [
58
+ "eslint --fix",
59
+ "prettier --write"
60
+ ]
61
+ },
62
+ "devDependencies": {
63
+ "@eslint/js": "^10.0.1",
64
+ "@inertiajs/vue3": "^3.7.0",
65
+ "@playwright/test": "^1.62.1",
66
+ "@storybook/addon-a11y": "^10.6.0",
67
+ "@storybook/addon-docs": "^10.6.0",
68
+ "@storybook/vue3-vite": "^10.6.0",
69
+ "@tailwindcss/postcss": "^4.3.3",
70
+ "@vitejs/plugin-vue": "^6.0.8",
71
+ "@vue/test-utils": "^2.5.0",
72
+ "autoprefixer": "^10.5.4",
73
+ "concurrently": "^10.0.5",
74
+ "eslint": "^10.9.1",
75
+ "eslint-plugin-storybook": "^10.5.10",
76
+ "eslint-plugin-vue": "^10.10.0",
77
+ "happy-dom": "^20.14.0",
78
+ "husky": "^9.1.7",
79
+ "lint-staged": "^17.4.1",
80
+ "playwright": "^1.62.1",
81
+ "postcss": "^8.5.26",
82
+ "prettier": "^3.9.6",
83
+ "storybook": "^10.5.10",
84
+ "tailwindcss": "^4.3.3",
85
+ "vitest": "^5.0.0",
86
+ "vue": "^3.5.42",
87
+ "wait-on": "^9.1.0"
88
+ }
85
89
  }
package/pages.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "$comment": "Generated by scripts/generate-page-manifest.js from src/pages. Every name here resolves to a component; a name a backend renders that is not here is a blank screen.",
3
+ "version": "0.11.3",
4
+ "pages": [
5
+ "Escalated/Admin/ApiTokens/Index",
6
+ "Escalated/Admin/AuditLog/Index",
7
+ "Escalated/Admin/Automations/Form",
8
+ "Escalated/Admin/Automations/Index",
9
+ "Escalated/Admin/BusinessHours/Form",
10
+ "Escalated/Admin/BusinessHours/Index",
11
+ "Escalated/Admin/CannedResponses/Index",
12
+ "Escalated/Admin/Capacity/Index",
13
+ "Escalated/Admin/CustomFields/Form",
14
+ "Escalated/Admin/CustomFields/Index",
15
+ "Escalated/Admin/CustomObjects/Form",
16
+ "Escalated/Admin/CustomObjects/Index",
17
+ "Escalated/Admin/CustomObjects/Records",
18
+ "Escalated/Admin/Departments/Form",
19
+ "Escalated/Admin/Departments/Index",
20
+ "Escalated/Admin/EscalationRules/Form",
21
+ "Escalated/Admin/EscalationRules/Index",
22
+ "Escalated/Admin/Import/Connect",
23
+ "Escalated/Admin/Import/Index",
24
+ "Escalated/Admin/Import/Mapping",
25
+ "Escalated/Admin/Import/Progress",
26
+ "Escalated/Admin/Import/Review",
27
+ "Escalated/Admin/KnowledgeBase/Articles/Form",
28
+ "Escalated/Admin/KnowledgeBase/Articles/Index",
29
+ "Escalated/Admin/KnowledgeBase/Categories/Index",
30
+ "Escalated/Admin/Macros/Index",
31
+ "Escalated/Admin/Newsletters/Compose",
32
+ "Escalated/Admin/Newsletters/Edit",
33
+ "Escalated/Admin/Newsletters/Index",
34
+ "Escalated/Admin/Newsletters/Lists/Create",
35
+ "Escalated/Admin/Newsletters/Lists/Index",
36
+ "Escalated/Admin/Newsletters/Lists/Show",
37
+ "Escalated/Admin/Newsletters/Settings",
38
+ "Escalated/Admin/Newsletters/Show",
39
+ "Escalated/Admin/Newsletters/Templates/Create",
40
+ "Escalated/Admin/Newsletters/Templates/Index",
41
+ "Escalated/Admin/Newsletters/Templates/Show",
42
+ "Escalated/Admin/Plugins/Index",
43
+ "Escalated/Admin/Reports",
44
+ "Escalated/Admin/Reports/AgentDetail",
45
+ "Escalated/Admin/Reports/AgentMetrics",
46
+ "Escalated/Admin/Reports/AgentRanking",
47
+ "Escalated/Admin/Reports/Cohorts",
48
+ "Escalated/Admin/Reports/Comparison",
49
+ "Escalated/Admin/Reports/CsatReport",
50
+ "Escalated/Admin/Reports/Dashboard",
51
+ "Escalated/Admin/Reports/ResolutionTimes",
52
+ "Escalated/Admin/Reports/ResponseTimes",
53
+ "Escalated/Admin/Reports/SlaReport",
54
+ "Escalated/Admin/Reports/SlaTrends",
55
+ "Escalated/Admin/Roles/Form",
56
+ "Escalated/Admin/Roles/Index",
57
+ "Escalated/Admin/Settings",
58
+ "Escalated/Admin/Settings/CsatSettings",
59
+ "Escalated/Admin/Settings/DataRetention",
60
+ "Escalated/Admin/Settings/DatabaseConnection",
61
+ "Escalated/Admin/Settings/EmailSettings",
62
+ "Escalated/Admin/Settings/PublicTickets",
63
+ "Escalated/Admin/Settings/Sandbox",
64
+ "Escalated/Admin/Settings/SsoSettings",
65
+ "Escalated/Admin/Settings/TwoFactor",
66
+ "Escalated/Admin/Skills/Form",
67
+ "Escalated/Admin/Skills/Index",
68
+ "Escalated/Admin/SlaPolicies/Form",
69
+ "Escalated/Admin/SlaPolicies/Index",
70
+ "Escalated/Admin/Statuses/Form",
71
+ "Escalated/Admin/Statuses/Index",
72
+ "Escalated/Admin/Tags/Index",
73
+ "Escalated/Admin/Tickets/Index",
74
+ "Escalated/Admin/Tickets/Show",
75
+ "Escalated/Admin/Users/Index",
76
+ "Escalated/Admin/Webhooks/DeliveryLog",
77
+ "Escalated/Admin/Webhooks/Form",
78
+ "Escalated/Admin/Webhooks/Index",
79
+ "Escalated/Admin/Workflows/Builder",
80
+ "Escalated/Admin/Workflows/Form",
81
+ "Escalated/Admin/Workflows/Index",
82
+ "Escalated/Admin/Workflows/Logs",
83
+ "Escalated/Agent/Dashboard",
84
+ "Escalated/Agent/TicketIndex",
85
+ "Escalated/Agent/TicketShow",
86
+ "Escalated/Customer/Create",
87
+ "Escalated/Customer/Index",
88
+ "Escalated/Customer/KnowledgeBase/Article",
89
+ "Escalated/Customer/KnowledgeBase/Index",
90
+ "Escalated/Customer/Show",
91
+ "Escalated/Guest/Create",
92
+ "Escalated/Guest/Show",
93
+ "Escalated/Plugin/Page"
94
+ ]
95
+ }
package/src/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  // Plugin
2
2
  export { EscalatedPlugin } from './plugin';
3
3
 
4
+ // Page resolution. A page name with no component behind it resolves to nothing
5
+ // in Inertia -- a 200 and a blank panel -- so this says so instead.
6
+ export { resolveEscalatedPage, isEscalatedPage, escalatedPages, suggestPages } from './resolvePage';
7
+
4
8
  // Components
5
9
  export { default as ActiveChatsPanel } from './components/ActiveChatsPanel.vue';
6
10
  export { default as ActivityTimeline } from './components/ActivityTimeline.vue';
@@ -0,0 +1,160 @@
1
+ import manifest from '../pages.json';
2
+
3
+ /**
4
+ * Resolve an Escalated page name to its component, loudly.
5
+ *
6
+ * Inertia resolves a page name to a component, and a name with nothing behind
7
+ * it is not an error: `resolve()` returns undefined, Vue renders nothing, and
8
+ * the panel comes up blank. It looks like a permissions problem or an empty
9
+ * dataset, and the request was a 200 the whole time — which is how four screens
10
+ * shipped blank across this package and its backends without anyone noticing.
11
+ *
12
+ * This wraps the lookup so an unresolvable name says so. In development it
13
+ * throws, naming what was asked for and the nearest thing that exists; in
14
+ * production it logs and hands back a component that renders the same message,
15
+ * because a panel that explains itself beats a panel that is empty.
16
+ *
17
+ * Wire it into the host's Inertia setup:
18
+ *
19
+ * import { resolveEscalatedPage } from '@escalated-dev/escalated';
20
+ *
21
+ * createInertiaApp({
22
+ * resolve: (name) =>
23
+ * resolveEscalatedPage(name, () =>
24
+ * resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**')),
25
+ * ),
26
+ * });
27
+ *
28
+ * @param {string} name The page name the backend rendered.
29
+ * @param {Function} [fallback] Resolver for names this package does not own.
30
+ * @param {Object} [options]
31
+ * @param {boolean} [options.throwOnMissing] Defaults to true outside production.
32
+ * @returns {Promise<Object>|Object}
33
+ */
34
+ export function resolveEscalatedPage(name, fallback, options = {}) {
35
+ const pages = import.meta.glob('./pages/**/*.vue');
36
+ const path = `./pages/${String(name ?? '').replace(/^Escalated\//, '')}.vue`;
37
+
38
+ if (pages[path]) {
39
+ return pages[path]();
40
+ }
41
+
42
+ // Not ours: a host page, or a plugin's. Only names this package claims are
43
+ // its responsibility to complain about.
44
+ if (!isEscalatedPage(name)) {
45
+ if (fallback) {
46
+ return fallback(name);
47
+ }
48
+
49
+ return missing(name, options);
50
+ }
51
+
52
+ return missing(name, options);
53
+ }
54
+
55
+ /** Whether a name is one this package is supposed to own. */
56
+ export function isEscalatedPage(name) {
57
+ return String(name ?? '').startsWith('Escalated/');
58
+ }
59
+
60
+ /** Every page name this package can resolve. */
61
+ export function escalatedPages() {
62
+ return [...manifest.pages];
63
+ }
64
+
65
+ /**
66
+ * Names close enough to be the one that was meant.
67
+ *
68
+ * Nearly every real instance of this has been a naming difference rather than
69
+ * an absent screen — `CohortAnalysis` for `Cohorts`, `New` for `Form` — so the
70
+ * message is far more useful when it can point at the likely intent.
71
+ */
72
+ export function suggestPages(name, limit = 3) {
73
+ const wanted = segments(name);
74
+
75
+ if (!wanted.length) {
76
+ return [];
77
+ }
78
+
79
+ return manifest.pages
80
+ .map((page) => ({ page, score: similarity(wanted, segments(page)) }))
81
+ .filter((entry) => entry.score > 0)
82
+ .sort((a, b) => b.score - a.score || a.page.localeCompare(b.page))
83
+ .slice(0, limit)
84
+ .map((entry) => entry.page);
85
+ }
86
+
87
+ function segments(name) {
88
+ return String(name ?? '')
89
+ .toLowerCase()
90
+ .split('/')
91
+ .filter(Boolean);
92
+ }
93
+
94
+ /**
95
+ * How much two page paths have in common.
96
+ *
97
+ * Segment overlap rather than leaf equality: nearly every page ends in `Index`
98
+ * or `Form`, so matching on the last segment alone ranks half the package
99
+ * equally. What actually distinguishes the intended page is the path around it
100
+ * -- `Admin/KB/Articles/Index` and `Admin/KnowledgeBase/Articles/Index` share
101
+ * three segments and differ in one.
102
+ */
103
+ function similarity(wanted, candidate) {
104
+ const shared = wanted.filter((part) => candidate.includes(part)).length;
105
+
106
+ if (!shared) {
107
+ return 0;
108
+ }
109
+
110
+ // The folder counts for more than the leaf, so a differently named screen
111
+ // in the right place outranks the same leaf somewhere unrelated.
112
+ const sameFolder = wanted.slice(0, -1).join('/') === candidate.slice(0, -1).join('/') ? 2 : 0;
113
+ const lengthPenalty = Math.abs(wanted.length - candidate.length) * 0.5;
114
+
115
+ // Every page in a folder shares its whole path, so the folder alone ranks
116
+ // them all equally -- `CohortAnalysis` would tie with `AgentMetrics` and
117
+ // lose on alphabet. How much of the name itself matches is what separates
118
+ // the one that was meant.
119
+ const leaf = commonPrefix(wanted[wanted.length - 1], candidate[candidate.length - 1]);
120
+
121
+ return shared + sameFolder + leaf - lengthPenalty;
122
+ }
123
+
124
+ /** How many leading characters two names share, as a fraction of the shorter. */
125
+ function commonPrefix(a = '', b = '') {
126
+ let i = 0;
127
+
128
+ while (i < a.length && i < b.length && a[i] === b[i]) {
129
+ i += 1;
130
+ }
131
+
132
+ return i / Math.max(1, Math.min(a.length, b.length));
133
+ }
134
+
135
+ function missing(name, options) {
136
+ const suggestions = suggestPages(name);
137
+ const message =
138
+ `Escalated: no page component for "${name}". ` +
139
+ (suggestions.length
140
+ ? `Did you mean ${suggestions.map((s) => `"${s}"`).join(', ')}?`
141
+ : 'It is not in this package; a host or plugin page needs its own resolver.');
142
+
143
+ const shouldThrow = options.throwOnMissing ?? !isProduction();
144
+
145
+ if (shouldThrow) {
146
+ throw new Error(message);
147
+ }
148
+
149
+ console.error(message);
150
+
151
+ return { render: () => null, name: 'EscalatedMissingPage', __escalatedMissing: message };
152
+ }
153
+
154
+ function isProduction() {
155
+ try {
156
+ return import.meta.env?.PROD === true;
157
+ } catch {
158
+ return false;
159
+ }
160
+ }