@abinnovision/payloadcms-wayfinder 1.0.0-beta.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/LICENSE +201 -0
- package/README.md +132 -0
- package/dist/admin/LinkLabelFeature.client.d.mts +5 -0
- package/dist/admin/LinkLabelFeature.client.mjs +35 -0
- package/dist/admin/index.d.mts +2 -0
- package/dist/admin/index.mjs +3 -0
- package/dist/config/collection-string-field.d.mts +20 -0
- package/dist/config/collection-string-field.mjs +33 -0
- package/dist/config/has-duplicates.d.mts +15 -0
- package/dist/config/has-duplicates.mjs +14 -0
- package/dist/config/index.d.mts +8 -0
- package/dist/config/index.mjs +8 -0
- package/dist/config/link-field.d.mts +35 -0
- package/dist/config/link-field.mjs +150 -0
- package/dist/config/load-mappings.d.mts +38 -0
- package/dist/config/load-mappings.mjs +60 -0
- package/dist/config/mapping-global.d.mts +34 -0
- package/dist/config/mapping-global.mjs +86 -0
- package/dist/config/plugin.d.mts +31 -0
- package/dist/config/plugin.mjs +36 -0
- package/dist/config/translations.d.mts +34 -0
- package/dist/config/translations.mjs +45 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +15 -0
- package/dist/lexical/index.d.mts +52 -0
- package/dist/lexical/index.mjs +57 -0
- package/dist/montage/index.d.mts +32 -0
- package/dist/montage/index.mjs +30 -0
- package/dist/pattern/define-links.d.mts +187 -0
- package/dist/pattern/define-links.mjs +41 -0
- package/dist/pattern/define-mappings.d.mts +15 -0
- package/dist/pattern/define-mappings.mjs +14 -0
- package/dist/pattern/derive-link-label.d.mts +23 -0
- package/dist/pattern/derive-link-label.mjs +35 -0
- package/dist/pattern/index.d.mts +8 -0
- package/dist/pattern/index.mjs +8 -0
- package/dist/pattern/matcher.d.mts +26 -0
- package/dist/pattern/matcher.mjs +74 -0
- package/dist/pattern/param-query-path.d.mts +42 -0
- package/dist/pattern/param-query-path.mjs +53 -0
- package/dist/pattern/resolver.d.mts +30 -0
- package/dist/pattern/resolver.mjs +104 -0
- package/dist/pattern/types.d.mts +169 -0
- package/dist/pattern/types.mjs +11 -0
- package/dist/runtime/build-href.d.mts +39 -0
- package/dist/runtime/build-href.mjs +73 -0
- package/dist/runtime/build-path.d.mts +32 -0
- package/dist/runtime/build-path.mjs +49 -0
- package/dist/runtime/diagnostics.d.mts +35 -0
- package/dist/runtime/index.d.mts +7 -0
- package/dist/runtime/index.mjs +6 -0
- package/dist/runtime/resolve-link.d.mts +63 -0
- package/dist/runtime/resolve-link.mjs +89 -0
- package/dist/runtime/resolve-path.d.mts +54 -0
- package/dist/runtime/resolve-path.mjs +108 -0
- package/dist/runtime/resolve-relationship-slug.d.mts +33 -0
- package/dist/runtime/resolve-relationship-slug.mjs +51 -0
- package/package.json +133 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { variantsOf } from "../pattern/define-links.mjs";
|
|
2
|
+
import { relationship, text } from "payload/shared";
|
|
3
|
+
//#region src/config/link-field.ts
|
|
4
|
+
/** Values a variant may claim in order to replace the built-in behaviour. */ const BUILTIN_VALUES = /* @__PURE__ */ new Set([
|
|
5
|
+
"none",
|
|
6
|
+
"reference",
|
|
7
|
+
"custom",
|
|
8
|
+
"same-page"
|
|
9
|
+
]);
|
|
10
|
+
const DEFAULT_LABELS = {
|
|
11
|
+
label: "Label",
|
|
12
|
+
none: "None",
|
|
13
|
+
reference: "Internal link",
|
|
14
|
+
custom: "Custom",
|
|
15
|
+
samePage: "Same page",
|
|
16
|
+
newTab: "Open in new tab",
|
|
17
|
+
referenceField: "Document to link to",
|
|
18
|
+
urlField: "Custom URL",
|
|
19
|
+
samePageField: "Section identifier"
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A link that can point at a document, an arbitrary URL, or an anchor.
|
|
23
|
+
*
|
|
24
|
+
* The stored value is routed by `resolveLink`, which reads the collection
|
|
25
|
+
* mapping — so a link authored once follows its target when that collection's
|
|
26
|
+
* URL pattern changes.
|
|
27
|
+
*
|
|
28
|
+
* Each conditional sub-field declares itself optional and re-implements
|
|
29
|
+
* required-ness inside `validate`. Payload validates hidden fields too, so a
|
|
30
|
+
* plain `required: true` on a field behind a condition blocks saving whenever
|
|
31
|
+
* a different link type is selected.
|
|
32
|
+
*
|
|
33
|
+
* @param args Link targets, extra variants and presentation overrides.
|
|
34
|
+
*/ /**
|
|
35
|
+
* Shows a variant's own fields only when that variant is selected.
|
|
36
|
+
*
|
|
37
|
+
* Spreading a member of the `Field` union widens its discriminated `admin`
|
|
38
|
+
* shape to the union's, which no longer assigns back — so the result is
|
|
39
|
+
* reassembled through a single narrow assertion here rather than at each of
|
|
40
|
+
* the four call sites this would otherwise need.
|
|
41
|
+
*
|
|
42
|
+
* @param field The variant's field.
|
|
43
|
+
* @param value The variant's own value.
|
|
44
|
+
*/ const withVariantCondition = (field, value) => {
|
|
45
|
+
const next = { ...field };
|
|
46
|
+
const admin = next;
|
|
47
|
+
admin.admin = {
|
|
48
|
+
...admin.admin,
|
|
49
|
+
condition: (_, siblingData) => siblingData.type === value
|
|
50
|
+
};
|
|
51
|
+
return next;
|
|
52
|
+
};
|
|
53
|
+
const linkField = (args) => {
|
|
54
|
+
const isRequired = args.required ?? true;
|
|
55
|
+
const withLabel = args.withLabel ?? false;
|
|
56
|
+
const localizedLabel = args.localizedLabel ?? true;
|
|
57
|
+
const labels = {
|
|
58
|
+
...DEFAULT_LABELS,
|
|
59
|
+
...args.labels
|
|
60
|
+
};
|
|
61
|
+
const variants = variantsOf(args);
|
|
62
|
+
const missingRequired = (type, value, siblingData) => siblingData.type === type && !value;
|
|
63
|
+
return {
|
|
64
|
+
name: "link",
|
|
65
|
+
type: "group",
|
|
66
|
+
...args.interfaceName ? { interfaceName: args.interfaceName } : {},
|
|
67
|
+
admin: { hideGutter: true },
|
|
68
|
+
fields: [
|
|
69
|
+
{
|
|
70
|
+
name: "label",
|
|
71
|
+
type: "text",
|
|
72
|
+
required: withLabel && isRequired,
|
|
73
|
+
hidden: !withLabel,
|
|
74
|
+
localized: localizedLabel,
|
|
75
|
+
label: labels.label
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: "row",
|
|
79
|
+
fields: [{
|
|
80
|
+
name: "type",
|
|
81
|
+
type: "radio",
|
|
82
|
+
admin: {
|
|
83
|
+
layout: "horizontal",
|
|
84
|
+
width: "50%"
|
|
85
|
+
},
|
|
86
|
+
defaultValue: isRequired ? "reference" : "none",
|
|
87
|
+
options: [...[
|
|
88
|
+
...!isRequired ? [{
|
|
89
|
+
label: labels.none,
|
|
90
|
+
value: "none"
|
|
91
|
+
}] : [],
|
|
92
|
+
{
|
|
93
|
+
label: labels.reference,
|
|
94
|
+
value: "reference"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
label: labels.custom,
|
|
98
|
+
value: "custom"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: labels.samePage,
|
|
102
|
+
value: "same-page"
|
|
103
|
+
}
|
|
104
|
+
].map((builtin) => variants.find((it) => it.value === builtin.value) ?? builtin), ...variants.filter((it) => !BUILTIN_VALUES.has(it.value))].map((it) => ({
|
|
105
|
+
label: it.label,
|
|
106
|
+
value: it.value
|
|
107
|
+
}))
|
|
108
|
+
}, {
|
|
109
|
+
name: "newTab",
|
|
110
|
+
type: "checkbox",
|
|
111
|
+
label: labels.newTab,
|
|
112
|
+
admin: {
|
|
113
|
+
style: { alignSelf: "flex-end" },
|
|
114
|
+
width: "50%",
|
|
115
|
+
condition: (_, siblingData) => siblingData.type !== "same-page"
|
|
116
|
+
}
|
|
117
|
+
}]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "reference",
|
|
121
|
+
type: "relationship",
|
|
122
|
+
label: labels.referenceField,
|
|
123
|
+
admin: { condition: (_, siblingData) => siblingData.type === "reference" },
|
|
124
|
+
relationTo: args.relationTo,
|
|
125
|
+
hasMany: false,
|
|
126
|
+
required: false,
|
|
127
|
+
validate: (value, ctx) => missingRequired("reference", value, ctx.siblingData) ? ctx.req.t("validation:required") : relationship(value, ctx)
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "url",
|
|
131
|
+
type: "text",
|
|
132
|
+
label: labels.urlField,
|
|
133
|
+
admin: { condition: (_, siblingData) => siblingData.type === "custom" },
|
|
134
|
+
required: false,
|
|
135
|
+
validate: (value, ctx) => missingRequired("custom", value, ctx.siblingData) ? ctx.req.t("validation:required") : text(value, ctx)
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "samePageIdentifier",
|
|
139
|
+
type: "text",
|
|
140
|
+
label: labels.samePageField,
|
|
141
|
+
admin: { condition: (_, siblingData) => siblingData.type === "same-page" },
|
|
142
|
+
required: false,
|
|
143
|
+
validate: (value, ctx) => missingRequired("same-page", value, ctx.siblingData) ? ctx.req.t("validation:required") : text(value, ctx)
|
|
144
|
+
},
|
|
145
|
+
...variants.flatMap((variant) => (variant.fields ?? []).map((field) => withVariantCondition(field, variant.value)))
|
|
146
|
+
]
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
//#endregion
|
|
150
|
+
export { linkField };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PayloadCollectionMappingResolved } from "../pattern/types.mjs";
|
|
2
|
+
import { Payload } from "payload";
|
|
3
|
+
//#region src/config/load-mappings.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Somewhere to keep compiled mappings between reads.
|
|
6
|
+
*
|
|
7
|
+
* This caches compilation, not the read: the key is derived from what the read
|
|
8
|
+
* returned, which is what makes it impossible to serve a stale mapping, and
|
|
9
|
+
* also means the read always happens. To skip the read as well, wrap the whole
|
|
10
|
+
* `loadMappings` call in your own framework cache and invalidate it from
|
|
11
|
+
* `createMappingGlobal({ onChange })`.
|
|
12
|
+
*/
|
|
13
|
+
interface MappingCache {
|
|
14
|
+
get: (key: string) => PayloadCollectionMappingResolved[] | undefined;
|
|
15
|
+
set: (key: string, value: PayloadCollectionMappingResolved[]) => void;
|
|
16
|
+
}
|
|
17
|
+
interface LoadMappingsArgs {
|
|
18
|
+
payload: Payload;
|
|
19
|
+
globalSlug?: string;
|
|
20
|
+
/** Must match what {@link createMappingGlobal} was given. */
|
|
21
|
+
localized?: boolean;
|
|
22
|
+
/** Reuses compiled patterns across reads. @see MappingCache */
|
|
23
|
+
cache?: MappingCache;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Reads the mapping global and compiles it.
|
|
27
|
+
*
|
|
28
|
+
* Returns an empty list rather than throwing when the global has never been
|
|
29
|
+
* saved, which is the state every project is in on its first boot. A missing
|
|
30
|
+
* row, an unregistered collection or a locale with no pattern is skipped for
|
|
31
|
+
* the same reason: routing should degrade to "nothing matches", not to a
|
|
32
|
+
* crash on the way to the admin panel where an editor would fix it.
|
|
33
|
+
*
|
|
34
|
+
* @param args The Payload instance and mapping-global settings.
|
|
35
|
+
*/
|
|
36
|
+
declare const loadMappings: (args: LoadMappingsArgs) => Promise<PayloadCollectionMappingResolved[]>;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { LoadMappingsArgs, MappingCache, loadMappings };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { resolveCollectionMapping } from "../pattern/resolver.mjs";
|
|
2
|
+
import "./mapping-global.mjs";
|
|
3
|
+
//#region src/config/load-mappings.ts
|
|
4
|
+
/** Process-lifetime memo, used when no cache is supplied. */ const createMemoryCache = () => {
|
|
5
|
+
const store = /* @__PURE__ */ new Map();
|
|
6
|
+
return {
|
|
7
|
+
get: (key) => store.get(key),
|
|
8
|
+
set: (key, value) => void store.set(key, value)
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
const defaultCache = createMemoryCache();
|
|
12
|
+
/**
|
|
13
|
+
* Reads the mapping global and compiles it.
|
|
14
|
+
*
|
|
15
|
+
* Returns an empty list rather than throwing when the global has never been
|
|
16
|
+
* saved, which is the state every project is in on its first boot. A missing
|
|
17
|
+
* row, an unregistered collection or a locale with no pattern is skipped for
|
|
18
|
+
* the same reason: routing should degrade to "nothing matches", not to a
|
|
19
|
+
* crash on the way to the admin panel where an editor would fix it.
|
|
20
|
+
*
|
|
21
|
+
* @param args The Payload instance and mapping-global settings.
|
|
22
|
+
*/ const loadMappings = async (args) => {
|
|
23
|
+
const localized = args.localized ?? true;
|
|
24
|
+
const cache = args.cache ?? defaultCache;
|
|
25
|
+
const global = await args.payload.findGlobal({
|
|
26
|
+
slug: args.globalSlug ?? "collections-mapping",
|
|
27
|
+
depth: 0,
|
|
28
|
+
overrideAccess: true,
|
|
29
|
+
...localized ? { locale: "all" } : {}
|
|
30
|
+
});
|
|
31
|
+
const usable = (Array.isArray(global?.collections) ? global.collections : []).flatMap((row) => {
|
|
32
|
+
const collection = row.collectionName;
|
|
33
|
+
const path = row.path;
|
|
34
|
+
if (typeof collection !== "string" || collection === "") return [];
|
|
35
|
+
if (typeof path === "string") return [{
|
|
36
|
+
collection,
|
|
37
|
+
path
|
|
38
|
+
}];
|
|
39
|
+
if (!path || typeof path !== "object") return [];
|
|
40
|
+
const patterns = Object.fromEntries(Object.entries(path).flatMap(([locale, pattern]) => typeof pattern === "string" && pattern !== "" ? [[locale, pattern]] : []));
|
|
41
|
+
return Object.keys(patterns).length > 0 ? [{
|
|
42
|
+
collection,
|
|
43
|
+
path: patterns
|
|
44
|
+
}] : [];
|
|
45
|
+
});
|
|
46
|
+
const key = JSON.stringify(usable);
|
|
47
|
+
const cached = cache.get(key);
|
|
48
|
+
if (cached) return cached;
|
|
49
|
+
const compiled = usable.flatMap((mapping) => {
|
|
50
|
+
try {
|
|
51
|
+
return [resolveCollectionMapping(mapping)];
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
cache.set(key, compiled);
|
|
57
|
+
return compiled;
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
export { loadMappings };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { GlobalConfig } from "payload";
|
|
2
|
+
//#region src/config/mapping-global.d.ts
|
|
3
|
+
/** The global the mapping is authored in, unless overridden. */
|
|
4
|
+
declare const DEFAULT_MAPPING_GLOBAL_SLUG = "collections-mapping";
|
|
5
|
+
interface CreateMappingGlobalArgs {
|
|
6
|
+
/** Defaults to {@link DEFAULT_MAPPING_GLOBAL_SLUG}. */
|
|
7
|
+
globalSlug?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Whether path patterns differ per locale. Turn it off for a project with
|
|
10
|
+
* no `localization` block — Payload would otherwise return a scalar where
|
|
11
|
+
* a per-locale record is expected.
|
|
12
|
+
*/
|
|
13
|
+
localized?: boolean;
|
|
14
|
+
/** Fallback identifier field for relationship parameters. */
|
|
15
|
+
identifierField?: string;
|
|
16
|
+
label?: GlobalConfig["label"];
|
|
17
|
+
adminGroup?: string;
|
|
18
|
+
access?: GlobalConfig["access"];
|
|
19
|
+
/** Generated-type name for the array rows. Unset by default. */
|
|
20
|
+
interfaceName?: string;
|
|
21
|
+
/** Called after the mapping changes, for cache invalidation. */
|
|
22
|
+
onChange?: () => void | Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Builds the global that maps collections onto the URL patterns they serve.
|
|
26
|
+
*
|
|
27
|
+
* Routing lives in content rather than in code, so adding a page type is an
|
|
28
|
+
* editorial act. Everything else in the package reads what this produces.
|
|
29
|
+
*
|
|
30
|
+
* @param args Slug, localization and presentation overrides.
|
|
31
|
+
*/
|
|
32
|
+
declare const createMappingGlobal: (args?: CreateMappingGlobalArgs) => GlobalConfig;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { CreateMappingGlobalArgs, DEFAULT_MAPPING_GLOBAL_SLUG, createMappingGlobal };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { resolveParamQueryPath } from "../pattern/param-query-path.mjs";
|
|
2
|
+
import { translate } from "./translations.mjs";
|
|
3
|
+
import { createCollectionStringField } from "./collection-string-field.mjs";
|
|
4
|
+
import { hasDuplicates } from "./has-duplicates.mjs";
|
|
5
|
+
import { pathToRegexp } from "path-to-regexp";
|
|
6
|
+
import { text } from "payload/shared";
|
|
7
|
+
//#region src/config/mapping-global.ts
|
|
8
|
+
/** The global the mapping is authored in, unless overridden. */ const DEFAULT_MAPPING_GLOBAL_SLUG = "collections-mapping";
|
|
9
|
+
/**
|
|
10
|
+
* Builds the global that maps collections onto the URL patterns they serve.
|
|
11
|
+
*
|
|
12
|
+
* Routing lives in content rather than in code, so adding a page type is an
|
|
13
|
+
* editorial act. Everything else in the package reads what this produces.
|
|
14
|
+
*
|
|
15
|
+
* @param args Slug, localization and presentation overrides.
|
|
16
|
+
*/ const createMappingGlobal = (args = {}) => {
|
|
17
|
+
const pathField = {
|
|
18
|
+
type: "text",
|
|
19
|
+
name: "path",
|
|
20
|
+
required: true,
|
|
21
|
+
localized: args.localized ?? true,
|
|
22
|
+
admin: { description: "Path pattern, e.g. \"/:section/:slug\". Use \"/*slug\" for a collection whose identifier is a full path." },
|
|
23
|
+
validate: (value, opts) => {
|
|
24
|
+
const base = text(value, opts);
|
|
25
|
+
if (base !== true) return base;
|
|
26
|
+
const t = opts.req.t;
|
|
27
|
+
if (typeof value !== "string") return translate(t, "invalidPath");
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = pathToRegexp(value);
|
|
31
|
+
} catch {
|
|
32
|
+
return translate(t, "pathUnparseable");
|
|
33
|
+
}
|
|
34
|
+
if (parsed.keys.length === 0) return translate(t, "pathNeedsParameter");
|
|
35
|
+
const collectionName = opts.siblingData.collectionName;
|
|
36
|
+
if (!collectionName) return translate(t, "selectCollectionFirst");
|
|
37
|
+
const collection = opts.req.payload.collections[collectionName];
|
|
38
|
+
if (!collection) return translate(t, "selectCollectionFirst");
|
|
39
|
+
for (const key of parsed.keys) {
|
|
40
|
+
const resolved = resolveParamQueryPath({
|
|
41
|
+
config: collection.config,
|
|
42
|
+
param: key.name,
|
|
43
|
+
collections: opts.req.payload.collections,
|
|
44
|
+
...args.identifierField ? { identifierField: args.identifierField } : {}
|
|
45
|
+
});
|
|
46
|
+
if ("error" in resolved) return resolved.error;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
slug: args.globalSlug ?? "collections-mapping",
|
|
53
|
+
label: args.label ?? "Collections Mapping",
|
|
54
|
+
...args.access ? { access: args.access } : {},
|
|
55
|
+
admin: {
|
|
56
|
+
group: args.adminGroup ?? "Settings",
|
|
57
|
+
description: "Maps each collection onto the URL pattern its documents are served at. The last parameter identifies the document; earlier parameters narrow the lookup."
|
|
58
|
+
},
|
|
59
|
+
...args.onChange ? { hooks: { afterChange: [() => void args.onChange()] } } : {},
|
|
60
|
+
fields: [{
|
|
61
|
+
type: "array",
|
|
62
|
+
name: "collections",
|
|
63
|
+
minRows: 0,
|
|
64
|
+
required: true,
|
|
65
|
+
...args.interfaceName ? { interfaceName: args.interfaceName } : {},
|
|
66
|
+
validate: (value, opts) => {
|
|
67
|
+
const rows = Array.isArray(value) ? value : [];
|
|
68
|
+
const t = opts.req.t;
|
|
69
|
+
if (hasDuplicates(rows.map((it) => it.collectionName))) return translate(t, "duplicateCollection");
|
|
70
|
+
const paths = rows.flatMap((it) => {
|
|
71
|
+
const path = it.path;
|
|
72
|
+
if (typeof path === "string") return [path];
|
|
73
|
+
return path && typeof path === "object" ? Object.entries(path).map(([locale, pattern]) => `${locale}:${String(pattern)}`) : [];
|
|
74
|
+
});
|
|
75
|
+
if (hasDuplicates(paths)) return translate(t, "duplicatePath");
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
fields: [createCollectionStringField({
|
|
79
|
+
name: "collectionName",
|
|
80
|
+
required: true
|
|
81
|
+
}), pathField]
|
|
82
|
+
}]
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
86
|
+
export { DEFAULT_MAPPING_GLOBAL_SLUG, createMappingGlobal };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CreateMappingGlobalArgs } from "./mapping-global.mjs";
|
|
2
|
+
import { Plugin } from "payload";
|
|
3
|
+
//#region src/config/plugin.d.ts
|
|
4
|
+
interface WayfinderPluginArgs extends CreateMappingGlobalArgs {
|
|
5
|
+
/**
|
|
6
|
+
* Collections that can be linked to. Only used to warn about missing
|
|
7
|
+
* `defaultPopulate`; linking itself is governed by the link field.
|
|
8
|
+
*/
|
|
9
|
+
linkableCollections?: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Set when the project resolves references through its own index rather
|
|
12
|
+
* than a populated document. Suppresses the `defaultPopulate` warning,
|
|
13
|
+
* which would otherwise be noise for a deliberately depth-capped setup.
|
|
14
|
+
*/
|
|
15
|
+
resolvesReferencesExternally?: boolean;
|
|
16
|
+
/** Silences the startup checks. */
|
|
17
|
+
quiet?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Registers the mapping global and the package's admin translations.
|
|
21
|
+
*
|
|
22
|
+
* The single place the global's slug and localization are decided, so the
|
|
23
|
+
* write side and the read side cannot drift apart — `loadMappings` has to be
|
|
24
|
+
* told the same two things, and a mismatch means writing to one global and
|
|
25
|
+
* reading from another.
|
|
26
|
+
*
|
|
27
|
+
* @param args Mapping-global settings and startup-check inputs.
|
|
28
|
+
*/
|
|
29
|
+
declare const wayfinderPlugin: (args?: WayfinderPluginArgs) => Plugin;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { WayfinderPluginArgs, wayfinderPlugin };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { wayfinderTranslations } from "./translations.mjs";
|
|
2
|
+
import { createMappingGlobal } from "./mapping-global.mjs";
|
|
3
|
+
//#region src/config/plugin.ts
|
|
4
|
+
/**
|
|
5
|
+
* Registers the mapping global and the package's admin translations.
|
|
6
|
+
*
|
|
7
|
+
* The single place the global's slug and localization are decided, so the
|
|
8
|
+
* write side and the read side cannot drift apart — `loadMappings` has to be
|
|
9
|
+
* told the same two things, and a mismatch means writing to one global and
|
|
10
|
+
* reading from another.
|
|
11
|
+
*
|
|
12
|
+
* @param args Mapping-global settings and startup-check inputs.
|
|
13
|
+
*/ const wayfinderPlugin = (args = {}) => (incoming) => {
|
|
14
|
+
const config = {
|
|
15
|
+
...incoming,
|
|
16
|
+
globals: [...incoming.globals ?? [], createMappingGlobal(args)],
|
|
17
|
+
i18n: {
|
|
18
|
+
...incoming.i18n,
|
|
19
|
+
translations: {
|
|
20
|
+
...incoming.i18n?.translations,
|
|
21
|
+
...wayfinderTranslations
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
if (args.quiet) return config;
|
|
26
|
+
if (!args.resolvesReferencesExternally) {
|
|
27
|
+
const missing = (args.linkableCollections ?? []).filter((slug) => {
|
|
28
|
+
const collection = config.collections?.find((it) => it.slug === slug);
|
|
29
|
+
return collection && !collection.defaultPopulate;
|
|
30
|
+
});
|
|
31
|
+
if (missing.length > 0) console.warn(`[wayfinder] Linkable collections without \`defaultPopulate\`: ${missing.join(", ")}. Links to them break whenever the query depth runs out before the relationship is populated.`);
|
|
32
|
+
}
|
|
33
|
+
return config;
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { wayfinderPlugin };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/config/translations.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Admin-facing messages, keyed for Payload's translation lookup.
|
|
4
|
+
*
|
|
5
|
+
* Registered by the plugin. A project that builds the mapping global directly
|
|
6
|
+
* without the plugin still works — `req.t` returns the key's fallback — but
|
|
7
|
+
* sees these in English regardless of the admin locale.
|
|
8
|
+
*/
|
|
9
|
+
declare const wayfinderTranslations: {
|
|
10
|
+
readonly en: {
|
|
11
|
+
readonly wayfinder: {
|
|
12
|
+
readonly duplicateCollection: "Each collection may only be mapped once";
|
|
13
|
+
readonly duplicatePath: "Two collections may not share the same path pattern";
|
|
14
|
+
readonly invalidPath: "Path is invalid";
|
|
15
|
+
readonly pathUnparseable: "Path could not be parsed as a route pattern";
|
|
16
|
+
readonly pathNeedsParameter: "Path must contain at least one parameter";
|
|
17
|
+
readonly selectCollectionFirst: "Select a collection first";
|
|
18
|
+
readonly unknownCollection: "Unknown collection \"{{value}}\". Available: {{known}}";
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
readonly de: {
|
|
22
|
+
readonly wayfinder: {
|
|
23
|
+
readonly duplicateCollection: "Jede Collection darf nur einmal zugeordnet werden";
|
|
24
|
+
readonly duplicatePath: "Zwei Collections dürfen nicht dasselbe Pfadmuster verwenden";
|
|
25
|
+
readonly invalidPath: "Pfad ist ungültig";
|
|
26
|
+
readonly pathUnparseable: "Pfad konnte nicht als Routenmuster gelesen werden";
|
|
27
|
+
readonly pathNeedsParameter: "Pfad muss mindestens einen Parameter enthalten";
|
|
28
|
+
readonly selectCollectionFirst: "Bitte zuerst eine Collection wählen";
|
|
29
|
+
readonly unknownCollection: "Unbekannte Collection \"{{value}}\". Verfügbar: {{known}}";
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { wayfinderTranslations };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/config/translations.ts
|
|
2
|
+
/**
|
|
3
|
+
* Admin-facing messages, keyed for Payload's translation lookup.
|
|
4
|
+
*
|
|
5
|
+
* Registered by the plugin. A project that builds the mapping global directly
|
|
6
|
+
* without the plugin still works — `req.t` returns the key's fallback — but
|
|
7
|
+
* sees these in English regardless of the admin locale.
|
|
8
|
+
*/ const wayfinderTranslations = {
|
|
9
|
+
en: { wayfinder: {
|
|
10
|
+
duplicateCollection: "Each collection may only be mapped once",
|
|
11
|
+
duplicatePath: "Two collections may not share the same path pattern",
|
|
12
|
+
invalidPath: "Path is invalid",
|
|
13
|
+
pathUnparseable: "Path could not be parsed as a route pattern",
|
|
14
|
+
pathNeedsParameter: "Path must contain at least one parameter",
|
|
15
|
+
selectCollectionFirst: "Select a collection first",
|
|
16
|
+
unknownCollection: "Unknown collection \"{{value}}\". Available: {{known}}"
|
|
17
|
+
} },
|
|
18
|
+
de: { wayfinder: {
|
|
19
|
+
duplicateCollection: "Jede Collection darf nur einmal zugeordnet werden",
|
|
20
|
+
duplicatePath: "Zwei Collections dürfen nicht dasselbe Pfadmuster verwenden",
|
|
21
|
+
invalidPath: "Pfad ist ungültig",
|
|
22
|
+
pathUnparseable: "Pfad konnte nicht als Routenmuster gelesen werden",
|
|
23
|
+
pathNeedsParameter: "Pfad muss mindestens einen Parameter enthalten",
|
|
24
|
+
selectCollectionFirst: "Bitte zuerst eine Collection wählen",
|
|
25
|
+
unknownCollection: "Unbekannte Collection \"{{value}}\". Verfügbar: {{known}}"
|
|
26
|
+
} }
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Translates a wayfinder key, falling back to English.
|
|
30
|
+
*
|
|
31
|
+
* Payload returns the key itself when nothing is registered, which would put a
|
|
32
|
+
* bare `wayfinder:invalidPath` in front of an editor. This keeps the English
|
|
33
|
+
* sentence in that case.
|
|
34
|
+
*
|
|
35
|
+
* @param t Payload's translation function, when available.
|
|
36
|
+
* @param key The key within the `wayfinder` namespace.
|
|
37
|
+
* @param vars Interpolation values.
|
|
38
|
+
*/ const translate = (t, key, vars) => {
|
|
39
|
+
const fallback = wayfinderTranslations.en.wayfinder[key];
|
|
40
|
+
const translated = t?.(`wayfinder:${key}`, vars);
|
|
41
|
+
if (!translated || translated === `wayfinder:${key}`) return Object.entries(vars ?? {}).reduce((acc, [name, value]) => acc.replaceAll(`{{${name}}}`, value), fallback);
|
|
42
|
+
return translated;
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { translate, wayfinderTranslations };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseResolvedLink, BuiltinLinkVariant, Contributed, DEFAULT_LOCALE_KEY, DeclaredLinkVariant, FormatHref, LabelLike, LinkFieldData, PayloadCollectionMapping, PayloadCollectionMappingMatch, PayloadCollectionMappingResolved, PayloadCollectionMappingResolvers, PayloadCollectionMappingSpecificity, ResolvedLink } from "./pattern/types.mjs";
|
|
2
|
+
import { AnyLinkVariantDefinition, DataOfFields, LinkDataOf, LinkDeclaration, LinkVariantDefinition, LinkVariantSource, LinkVariantSpec, ResolvedLinkOf, VariantBuilder, defineLinks, variantsOf } from "./pattern/define-links.mjs";
|
|
3
|
+
import { defineMappings } from "./pattern/define-mappings.mjs";
|
|
4
|
+
import { deriveLinkLabel } from "./pattern/derive-link-label.mjs";
|
|
5
|
+
import { PayloadCollectionMatch, matchCollectionMappings } from "./pattern/matcher.mjs";
|
|
6
|
+
import { DEFAULT_IDENTIFIER_FIELD, RegisteredCollections, ResolveParamQueryPathArgs, resolveParamQueryPath } from "./pattern/param-query-path.mjs";
|
|
7
|
+
import { isRootWildcard, resolveCollectionMapping, resolversFor } from "./pattern/resolver.mjs";
|
|
8
|
+
import "./pattern/index.mjs";
|
|
9
|
+
import { BuildDiagnosticReason, Diagnostic, DiagnosticReason, OnDiagnostic, ResolveLinkDiagnosticReason, ResolvePathDiagnosticReason } from "./runtime/diagnostics.mjs";
|
|
10
|
+
import { BuildHrefArgs, LinkableDocument, buildHref, identityFormatHref } from "./runtime/build-href.mjs";
|
|
11
|
+
import { BuildPathArgs, buildPath } from "./runtime/build-path.mjs";
|
|
12
|
+
import { ResolveLinkArgs, ResolveReference, isAvailableLink, resolveLink } from "./runtime/resolve-link.mjs";
|
|
13
|
+
import { PayloadDocument, ResolvePathToDocumentArgs, ResolvePathWhere, ResolvedPath, resolvePathToDocument } from "./runtime/resolve-path.mjs";
|
|
14
|
+
import { ResolveRelationshipSlugArgs, resolveRelationshipSlug } from "./runtime/resolve-relationship-slug.mjs";
|
|
15
|
+
import "./runtime/index.mjs";
|
|
16
|
+
export { type AnyLinkVariantDefinition, type BaseResolvedLink, type BuildDiagnosticReason, type BuildHrefArgs, type BuildPathArgs, type BuiltinLinkVariant, type Contributed, DEFAULT_IDENTIFIER_FIELD, DEFAULT_LOCALE_KEY, type DataOfFields, type DeclaredLinkVariant, type Diagnostic, type DiagnosticReason, type FormatHref, type LabelLike, type LinkDataOf, type LinkDeclaration, type LinkFieldData, type LinkVariantDefinition, type LinkVariantSource, type LinkVariantSpec, type LinkableDocument, type OnDiagnostic, type PayloadCollectionMapping, type PayloadCollectionMappingMatch, type PayloadCollectionMappingResolved, type PayloadCollectionMappingResolvers, type PayloadCollectionMappingSpecificity, type PayloadCollectionMatch, type PayloadDocument, type RegisteredCollections, type ResolveLinkArgs, type ResolveLinkDiagnosticReason, type ResolveParamQueryPathArgs, type ResolvePathDiagnosticReason, type ResolvePathToDocumentArgs, type ResolvePathWhere, type ResolveReference, type ResolveRelationshipSlugArgs, type ResolvedLink, type ResolvedLinkOf, type ResolvedPath, type VariantBuilder, buildHref, buildPath, defineLinks, defineMappings, deriveLinkLabel, identityFormatHref, isAvailableLink, isRootWildcard, matchCollectionMappings, resolveCollectionMapping, resolveLink, resolveParamQueryPath, resolvePathToDocument, resolveRelationshipSlug, resolversFor, variantsOf };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineLinks, variantsOf } from "./pattern/define-links.mjs";
|
|
2
|
+
import { DEFAULT_LOCALE_KEY } from "./pattern/types.mjs";
|
|
3
|
+
import { isRootWildcard, resolveCollectionMapping, resolversFor } from "./pattern/resolver.mjs";
|
|
4
|
+
import { defineMappings } from "./pattern/define-mappings.mjs";
|
|
5
|
+
import { deriveLinkLabel } from "./pattern/derive-link-label.mjs";
|
|
6
|
+
import { matchCollectionMappings } from "./pattern/matcher.mjs";
|
|
7
|
+
import { DEFAULT_IDENTIFIER_FIELD, resolveParamQueryPath } from "./pattern/param-query-path.mjs";
|
|
8
|
+
import "./pattern/index.mjs";
|
|
9
|
+
import { buildHref, identityFormatHref } from "./runtime/build-href.mjs";
|
|
10
|
+
import { buildPath } from "./runtime/build-path.mjs";
|
|
11
|
+
import { isAvailableLink, resolveLink } from "./runtime/resolve-link.mjs";
|
|
12
|
+
import { resolvePathToDocument } from "./runtime/resolve-path.mjs";
|
|
13
|
+
import { resolveRelationshipSlug } from "./runtime/resolve-relationship-slug.mjs";
|
|
14
|
+
import "./runtime/index.mjs";
|
|
15
|
+
export { DEFAULT_IDENTIFIER_FIELD, DEFAULT_LOCALE_KEY, buildHref, buildPath, defineLinks, defineMappings, deriveLinkLabel, identityFormatHref, isAvailableLink, isRootWildcard, matchCollectionMappings, resolveCollectionMapping, resolveLink, resolveParamQueryPath, resolvePathToDocument, resolveRelationshipSlug, resolversFor, variantsOf };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { BaseResolvedLink, LinkFieldData, ResolvedLink } from "../pattern/types.mjs";
|
|
2
|
+
import { LinkDeclaration, ResolvedLinkOf } from "../pattern/define-links.mjs";
|
|
3
|
+
import { LinkFieldArgs } from "../config/link-field.mjs";
|
|
4
|
+
import { ResolveLinkArgs } from "../runtime/resolve-link.mjs";
|
|
5
|
+
import { LinkFeature } from "@payloadcms/richtext-lexical";
|
|
6
|
+
//#region src/lexical/index.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Gives every created or edited link a top-level `label` derived from its
|
|
9
|
+
* destination, so the floating link editor shows a useful hover preview
|
|
10
|
+
* instead of a blank one.
|
|
11
|
+
*/
|
|
12
|
+
declare const linkLabelFeature: import("@payloadcms/richtext-lexical").FeatureProviderProviderServer<undefined, undefined, undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* Replaces Lexical's link fields with the wayfinder link field, so links
|
|
15
|
+
* written in rich text route through the collection mapping exactly like links
|
|
16
|
+
* authored in a block.
|
|
17
|
+
*
|
|
18
|
+
* @param args The same arguments as the standalone link field.
|
|
19
|
+
*/
|
|
20
|
+
declare const wayfinderLinkFeature: <TDeclaration extends LinkDeclaration = LinkDeclaration>(args: LinkFieldArgs<TDeclaration>) => ReturnType<typeof LinkFeature>;
|
|
21
|
+
/**
|
|
22
|
+
* The two shapes a link node's fields arrive in.
|
|
23
|
+
*
|
|
24
|
+
* A node written by {@link wayfinderLinkFeature} nests the group under `link`.
|
|
25
|
+
* A node written by Lexical's stock link feature stores `linkType` and `doc`
|
|
26
|
+
* at the top level, which existing content will still hold.
|
|
27
|
+
*/
|
|
28
|
+
interface SerializedLinkFields {
|
|
29
|
+
link?: LinkFieldData;
|
|
30
|
+
linkType?: "custom" | "internal";
|
|
31
|
+
url?: string | null;
|
|
32
|
+
newTab?: boolean | null;
|
|
33
|
+
doc?: {
|
|
34
|
+
relationTo: string;
|
|
35
|
+
value: string | {
|
|
36
|
+
id: string;
|
|
37
|
+
};
|
|
38
|
+
} | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolves a rich-text link node to an href.
|
|
42
|
+
*
|
|
43
|
+
* Returns null when the node points nowhere resolvable, so a converter can
|
|
44
|
+
* render the text without an anchor rather than emitting a dead one.
|
|
45
|
+
*
|
|
46
|
+
* @param args The node's fields plus the usual link-resolution arguments.
|
|
47
|
+
*/
|
|
48
|
+
declare const resolveLinkNode: <TExtra = object, TDeclaration extends LinkDeclaration = LinkDeclaration>(args: Omit<ResolveLinkArgs<TExtra, TDeclaration>, "link"> & {
|
|
49
|
+
fields: SerializedLinkFields | undefined;
|
|
50
|
+
}) => BaseResolvedLink | ResolvedLink<object> | ResolvedLinkOf<TDeclaration> | null;
|
|
51
|
+
//#endregion
|
|
52
|
+
export { linkLabelFeature, resolveLinkNode, wayfinderLinkFeature };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { resolveLink } from "../runtime/resolve-link.mjs";
|
|
2
|
+
import { linkField } from "../config/link-field.mjs";
|
|
3
|
+
import { LinkFeature, createServerFeature } from "@payloadcms/richtext-lexical";
|
|
4
|
+
//#region src/lexical/index.ts
|
|
5
|
+
/** Where the admin bundle finds the label plugin. */ const LABEL_FEATURE_CLIENT = "@abinnovision/payloadcms-wayfinder/admin#LinkLabelFeatureClient";
|
|
6
|
+
/**
|
|
7
|
+
* Gives every created or edited link a top-level `label` derived from its
|
|
8
|
+
* destination, so the floating link editor shows a useful hover preview
|
|
9
|
+
* instead of a blank one.
|
|
10
|
+
*/ const linkLabelFeature = createServerFeature({
|
|
11
|
+
key: "wayfinder-link-label",
|
|
12
|
+
feature: () => ({
|
|
13
|
+
ClientFeature: LABEL_FEATURE_CLIENT,
|
|
14
|
+
markdownTransformers: []
|
|
15
|
+
})
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Replaces Lexical's link fields with the wayfinder link field, so links
|
|
19
|
+
* written in rich text route through the collection mapping exactly like links
|
|
20
|
+
* authored in a block.
|
|
21
|
+
*
|
|
22
|
+
* @param args The same arguments as the standalone link field.
|
|
23
|
+
*/ const wayfinderLinkFeature = (args) => LinkFeature({ fields: () => [linkField(args)] });
|
|
24
|
+
/**
|
|
25
|
+
* Normalises a link node's fields into the link field's own shape.
|
|
26
|
+
*
|
|
27
|
+
* @param fields The node's `fields` object.
|
|
28
|
+
*/ const normaliseNodeFields = (fields) => {
|
|
29
|
+
if (!fields) return;
|
|
30
|
+
if (fields.link) return fields.link;
|
|
31
|
+
if (fields.linkType === "internal" && fields.doc) return {
|
|
32
|
+
type: "reference",
|
|
33
|
+
reference: fields.doc,
|
|
34
|
+
newTab: fields.newTab ?? null
|
|
35
|
+
};
|
|
36
|
+
if (fields.url) return {
|
|
37
|
+
type: "custom",
|
|
38
|
+
url: fields.url,
|
|
39
|
+
newTab: fields.newTab ?? null
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Resolves a rich-text link node to an href.
|
|
44
|
+
*
|
|
45
|
+
* Returns null when the node points nowhere resolvable, so a converter can
|
|
46
|
+
* render the text without an anchor rather than emitting a dead one.
|
|
47
|
+
*
|
|
48
|
+
* @param args The node's fields plus the usual link-resolution arguments.
|
|
49
|
+
*/ const resolveLinkNode = (args) => {
|
|
50
|
+
const link = normaliseNodeFields(args.fields);
|
|
51
|
+
return resolveLink({
|
|
52
|
+
...args,
|
|
53
|
+
link
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { linkLabelFeature, resolveLinkNode, wayfinderLinkFeature };
|