create-nevela 0.7.0 → 0.8.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.
- package/index.mjs +1 -0
- package/package.json +1 -1
- package/project.mjs +1 -1
- package/template/laravel/config/nevela.php +25 -3
- package/template/laravel/src/Access/Permissions.php +36 -5
- package/template/laravel/src/Access/SampleUsers.php +1 -3
- package/template/laravel/src/Concerns/Trashable.php +23 -0
- package/template/laravel/src/Console/AdoptCommand.php +139 -0
- package/template/laravel/src/Console/GenerateCommand.php +13 -3
- package/template/laravel/src/Console/MakeResourceCommand.php +2 -0
- package/template/laravel/src/Console/SeedCommand.php +3 -1
- package/template/laravel/src/Console/TrashCommand.php +8 -0
- package/template/laravel/src/Console/UserCommand.php +5 -1
- package/template/laravel/src/Generator/ResourceGenerator.php +229 -29
- package/template/laravel/src/Http/Access/UsersController.php +2 -4
- package/template/laravel/src/Http/ActionController.php +222 -0
- package/template/laravel/src/Http/PublicController.php +210 -0
- package/template/laravel/src/Http/PublicThrottle.php +48 -0
- package/template/laravel/src/Http/ResourceRequest.php +151 -2
- package/template/laravel/src/Http/TrashController.php +18 -2
- package/template/laravel/src/Http/UploadController.php +42 -1
- package/template/laravel/src/Media/Uploads.php +111 -0
- package/template/laravel/src/Nevela.php +54 -2
- package/template/laravel/src/NevelaServiceProvider.php +17 -1
- package/template/laravel/src/Support/Action.php +168 -0
- package/template/laravel/src/Support/Adoption.php +352 -0
- package/template/laravel/src/Support/Descriptor.php +59 -4
- package/template/laravel/src/Support/Fake.php +15 -1
- package/template/laravel/src/Support/Field.php +74 -3
- package/template/laravel/src/Support/Html.php +185 -0
- package/template/laravel/src/Support/PublicAccess.php +133 -0
- package/template/web/app/api/[...path]/route.ts +42 -9
- package/template/web/app/dashboard/actions.ts +34 -0
- package/template/web/app/globals.css +46 -0
- package/template/web/components/access/role-form.tsx +40 -5
- package/template/web/components/dashboard/bulk-create-grid.tsx +5 -2
- package/template/web/components/dashboard/fields/field-widget.tsx +5 -0
- package/template/web/components/dashboard/fields/file-cell.tsx +24 -3
- package/template/web/components/dashboard/fields/file-field.tsx +9 -5
- package/template/web/components/dashboard/fields/rich-text-field.tsx +261 -0
- package/template/web/components/dashboard/record-actions.tsx +7 -2
- package/template/web/components/dashboard/record-detail.tsx +9 -2
- package/template/web/components/dashboard/resource-action.tsx +281 -0
- package/template/web/components/dashboard/resource-table.tsx +15 -2
- package/template/web/components/dashboard/row-actions.tsx +24 -0
- package/template/web/components/dashboard/table-selection.tsx +19 -1
- package/template/web/lib/access.ts +3 -1
- package/template/web/lib/dashboard.ts +10 -0
- package/template/web/lib/extras.ts +66 -0
- package/template/web/lib/files.ts +14 -0
- package/template/web/lib/laravel.ts +60 -0
- package/template/web/lib/rich.ts +79 -0
- package/template/web/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Rich text: a text field whose value is HTML, written in an editor.
|
|
2
|
+
//
|
|
3
|
+
// Laravel is what makes it safe. Whatever a form, a script or a file sends is cleaned
|
|
4
|
+
// there against a list of tags (config/nevela.php, rich_text) before it is stored, and
|
|
5
|
+
// again when it is read, so the HTML that reaches this app is HTML that may be shown.
|
|
6
|
+
// Nothing here is a security boundary; tidy() below only keeps the editor neat.
|
|
7
|
+
|
|
8
|
+
/** The tags the editor's toolbar makes, which are the ones Laravel keeps by default. */
|
|
9
|
+
const KEPT = new Set(["p", "br", "strong", "em", "s", "a", "h2", "h3", "ul", "ol", "li", "blockquote"]);
|
|
10
|
+
const SAME_AS: Record<string, string> = { b: "strong", i: "em", strike: "s", del: "s" };
|
|
11
|
+
const DROPPED = new Set(["script", "style", "iframe", "object", "embed", "noscript", "template", "svg", "math", "head", "title", "meta", "link"]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether a field holds rich text. The generated descriptor says so with `rich: true`
|
|
15
|
+
* on a text field, a key Flare doesn't know and carries along untouched.
|
|
16
|
+
*/
|
|
17
|
+
export function isRich(def: { kind: string }): boolean {
|
|
18
|
+
return def.kind === "text" && (def as { rich?: boolean }).rich === true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The words alone, on one line: what a table cell shows of a rich text value. */
|
|
22
|
+
export function richToText(html: string, limit = 0): string {
|
|
23
|
+
const text = html
|
|
24
|
+
.replace(/<(br|\/p|\/h[1-6]|\/li|\/blockquote)\b[^>]*>/gi, " ")
|
|
25
|
+
.replace(/<[^>]*>/g, "")
|
|
26
|
+
.replace(/ /g, " ")
|
|
27
|
+
.replace(/</g, "<")
|
|
28
|
+
.replace(/>/g, ">")
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/�*39;|'/g, "'")
|
|
31
|
+
.replace(/&#(\d+);/g, (_, code: string) => String.fromCodePoint(Number(code)))
|
|
32
|
+
.replace(/&/g, "&")
|
|
33
|
+
.replace(/\s+/g, " ")
|
|
34
|
+
.trim();
|
|
35
|
+
return limit > 0 && text.length > limit ? `${text.slice(0, limit - 1).trimEnd()}…` : text;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Whether there is anything in it to read. An editor that was opened and left holds `<p><br></p>`. */
|
|
39
|
+
export function isBlankHtml(html: string): boolean {
|
|
40
|
+
return richToText(html) === "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What was pasted, with only what the toolbar could have made: a page copied from a
|
|
45
|
+
* word processor arrives with fonts, colours and spans that would be stripped on save
|
|
46
|
+
* anyway, and look wrong until then. Browser only.
|
|
47
|
+
*/
|
|
48
|
+
export function tidyHtml(html: string): string {
|
|
49
|
+
const body = new DOMParser().parseFromString(html, "text/html").body;
|
|
50
|
+
const escape = (text: string) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
51
|
+
const write = (node: Node): string => {
|
|
52
|
+
if (node.nodeType === Node.TEXT_NODE) return escape(node.textContent ?? "");
|
|
53
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return "";
|
|
54
|
+
const element = node as Element;
|
|
55
|
+
const raw = element.tagName.toLowerCase();
|
|
56
|
+
const tag = SAME_AS[raw] ?? raw;
|
|
57
|
+
if (DROPPED.has(tag)) return "";
|
|
58
|
+
const inner = Array.from(element.childNodes).map(write).join("");
|
|
59
|
+
if (!KEPT.has(tag)) return inner;
|
|
60
|
+
if (tag === "br") return "<br>";
|
|
61
|
+
if (tag === "a") {
|
|
62
|
+
const href = element.getAttribute("href") ?? "";
|
|
63
|
+
return /^(https?:|mailto:|\/|#)/i.test(href.trim()) ? `<a href="${escape(href.trim()).replace(/"/g, """)}">${inner}</a>` : inner;
|
|
64
|
+
}
|
|
65
|
+
return `<${tag}>${inner}</${tag}>`;
|
|
66
|
+
};
|
|
67
|
+
return Array.from(body.childNodes).map(write).join("");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** An address as it was typed, made into one a link can have. Empty when it can't be one. */
|
|
71
|
+
export function linkAddress(typed: string): string {
|
|
72
|
+
const address = typed.trim();
|
|
73
|
+
if (address === "") return "";
|
|
74
|
+
if (/^(https?:\/\/|mailto:|\/|#)/i.test(address)) return address;
|
|
75
|
+
// Any other scheme (javascript:, data:) is not somewhere a link goes.
|
|
76
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(address) && !/^[^/:]+:\d/.test(address)) return "";
|
|
77
|
+
if (/^[^\s@/]+@[^\s@/]+\.[^\s@/]+$/.test(address)) return `mailto:${address}`;
|
|
78
|
+
return `https://${address}`;
|
|
79
|
+
}
|