@escalated-dev/escalated 0.11.1 → 0.11.2
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 +86 -83
- package/src/index.js +4 -0
- package/src/resolvePage.js +160 -0
package/package.json
CHANGED
|
@@ -1,85 +1,88 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
2
|
+
"name": "@escalated-dev/escalated",
|
|
3
|
+
"version": "0.11.2",
|
|
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
|
+
],
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@inertiajs/vue3": "^1.0.0 || ^2.0.0 || ^3.0.0",
|
|
34
|
+
"vue": "^3.3.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@escalated-dev/locale": "^0.1.8"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"lint": "eslint src/ --max-warnings 0",
|
|
42
|
+
"lint:fix": "eslint src/ --fix",
|
|
43
|
+
"format": "prettier --write src/",
|
|
44
|
+
"format:check": "prettier --check src/",
|
|
45
|
+
"prepare": "husky",
|
|
46
|
+
"storybook": "storybook dev -p 6006",
|
|
47
|
+
"build-storybook": "storybook build",
|
|
48
|
+
"screenshot": "npx playwright test --config=playwright-screenshots.config.js",
|
|
49
|
+
"demo:serve": "node scripts/demo-server.js",
|
|
50
|
+
"demo:record": "node scripts/generate-demo.js",
|
|
51
|
+
"demo": "concurrently -k -s second \"npm run demo:serve\" \"wait-on http://localhost:3000 && npm run demo:record\"",
|
|
52
|
+
"pages:manifest": "node scripts/generate-page-manifest.js",
|
|
53
|
+
"pages:check": "node scripts/generate-page-manifest.js --check"
|
|
54
|
+
},
|
|
55
|
+
"lint-staged": {
|
|
56
|
+
"*.{js,vue}": [
|
|
57
|
+
"eslint --fix",
|
|
58
|
+
"prettier --write"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
63
|
+
"@inertiajs/vue3": "^3.7.0",
|
|
64
|
+
"@playwright/test": "^1.62.1",
|
|
65
|
+
"@storybook/addon-a11y": "^10.6.0",
|
|
66
|
+
"@storybook/addon-docs": "^10.6.0",
|
|
67
|
+
"@storybook/vue3-vite": "^10.6.0",
|
|
68
|
+
"@tailwindcss/postcss": "^4.3.3",
|
|
69
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
70
|
+
"@vue/test-utils": "^2.5.0",
|
|
71
|
+
"autoprefixer": "^10.5.4",
|
|
72
|
+
"concurrently": "^10.0.5",
|
|
73
|
+
"eslint": "^10.9.1",
|
|
74
|
+
"eslint-plugin-storybook": "^10.5.10",
|
|
75
|
+
"eslint-plugin-vue": "^10.10.0",
|
|
76
|
+
"happy-dom": "^20.14.0",
|
|
77
|
+
"husky": "^9.1.7",
|
|
78
|
+
"lint-staged": "^17.4.1",
|
|
79
|
+
"playwright": "^1.62.1",
|
|
80
|
+
"postcss": "^8.5.26",
|
|
81
|
+
"prettier": "^3.9.6",
|
|
82
|
+
"storybook": "^10.5.10",
|
|
83
|
+
"tailwindcss": "^4.3.3",
|
|
84
|
+
"vitest": "^5.0.0",
|
|
85
|
+
"vue": "^3.5.42",
|
|
86
|
+
"wait-on": "^9.1.0"
|
|
87
|
+
}
|
|
85
88
|
}
|
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
|
+
}
|