@multiplatform.one/frappe-ui 7.20.0 → 7.21.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/dist/cjs/FrappeForm.cjs +80 -50
- package/dist/cjs/FrappeForm.native.js +82 -55
- package/dist/cjs/FrappeForm.native.js.map +1 -1
- package/dist/cjs/FrappeListView.cjs +6 -1
- package/dist/cjs/FrappeListView.native.js +6 -1
- package/dist/cjs/FrappeListView.native.js.map +1 -1
- package/dist/cjs/FrappeTable.cjs +10 -1
- package/dist/cjs/FrappeTable.native.js +10 -1
- package/dist/cjs/FrappeTable.native.js.map +1 -1
- package/dist/cjs/formEngine/docPermissions.cjs +83 -0
- package/dist/cjs/formEngine/docPermissions.native.js +155 -0
- package/dist/cjs/formEngine/docPermissions.native.js.map +1 -0
- package/dist/cjs/formEngine/index.cjs +9 -1
- package/dist/cjs/formEngine/index.native.js +8 -0
- package/dist/cjs/formEngine/index.native.js.map +1 -1
- package/dist/cjs/formEngine/useDocRights.cjs +54 -0
- package/dist/cjs/formEngine/useDocRights.native.js +60 -0
- package/dist/cjs/formEngine/useDocRights.native.js.map +1 -0
- package/dist/cjs/formEngine/useSessionRoles.cjs +47 -18
- package/dist/cjs/formEngine/useSessionRoles.native.js +53 -19
- package/dist/cjs/formEngine/useSessionRoles.native.js.map +1 -1
- package/dist/cjs/index.cjs +6 -0
- package/dist/cjs/index.native.js +6 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/FrappeForm.mjs +82 -52
- package/dist/esm/FrappeForm.mjs.map +1 -1
- package/dist/esm/FrappeForm.native.js +84 -57
- package/dist/esm/FrappeForm.native.js.map +1 -1
- package/dist/esm/FrappeListView.mjs +6 -1
- package/dist/esm/FrappeListView.mjs.map +1 -1
- package/dist/esm/FrappeListView.native.js +6 -1
- package/dist/esm/FrappeListView.native.js.map +1 -1
- package/dist/esm/FrappeTable.mjs +10 -1
- package/dist/esm/FrappeTable.mjs.map +1 -1
- package/dist/esm/FrappeTable.native.js +10 -1
- package/dist/esm/FrappeTable.native.js.map +1 -1
- package/dist/esm/formEngine/docPermissions.mjs +55 -0
- package/dist/esm/formEngine/docPermissions.mjs.map +1 -0
- package/dist/esm/formEngine/docPermissions.native.js +124 -0
- package/dist/esm/formEngine/docPermissions.native.js.map +1 -0
- package/dist/esm/formEngine/index.mjs +4 -2
- package/dist/esm/formEngine/index.mjs.map +1 -1
- package/dist/esm/formEngine/index.native.js +4 -2
- package/dist/esm/formEngine/index.native.js.map +1 -1
- package/dist/esm/formEngine/useDocRights.mjs +29 -0
- package/dist/esm/formEngine/useDocRights.mjs.map +1 -0
- package/dist/esm/formEngine/useDocRights.native.js +32 -0
- package/dist/esm/formEngine/useDocRights.native.js.map +1 -0
- package/dist/esm/formEngine/useSessionRoles.mjs +47 -19
- package/dist/esm/formEngine/useSessionRoles.mjs.map +1 -1
- package/dist/esm/formEngine/useSessionRoles.native.js +53 -20
- package/dist/esm/formEngine/useSessionRoles.native.js.map +1 -1
- package/dist/esm/index.mjs +2 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +2 -2
- package/dist/esm/index.native.js.map +1 -1
- package/dist/jsx/FrappeForm.mjs +82 -52
- package/dist/jsx/FrappeForm.mjs.map +1 -1
- package/dist/jsx/FrappeForm.native.js +82 -55
- package/dist/jsx/FrappeForm.native.js.map +1 -1
- package/dist/jsx/FrappeListView.mjs +6 -1
- package/dist/jsx/FrappeListView.mjs.map +1 -1
- package/dist/jsx/FrappeListView.native.js +6 -1
- package/dist/jsx/FrappeListView.native.js.map +1 -1
- package/dist/jsx/FrappeTable.mjs +10 -1
- package/dist/jsx/FrappeTable.mjs.map +1 -1
- package/dist/jsx/FrappeTable.native.js +10 -1
- package/dist/jsx/FrappeTable.native.js.map +1 -1
- package/dist/jsx/formEngine/docPermissions.mjs +55 -0
- package/dist/jsx/formEngine/docPermissions.mjs.map +1 -0
- package/dist/jsx/formEngine/docPermissions.native.js +155 -0
- package/dist/jsx/formEngine/docPermissions.native.js.map +1 -0
- package/dist/jsx/formEngine/index.mjs +4 -2
- package/dist/jsx/formEngine/index.mjs.map +1 -1
- package/dist/jsx/formEngine/index.native.js +8 -0
- package/dist/jsx/formEngine/index.native.js.map +1 -1
- package/dist/jsx/formEngine/useDocRights.mjs +29 -0
- package/dist/jsx/formEngine/useDocRights.mjs.map +1 -0
- package/dist/jsx/formEngine/useDocRights.native.js +60 -0
- package/dist/jsx/formEngine/useDocRights.native.js.map +1 -0
- package/dist/jsx/formEngine/useSessionRoles.mjs +47 -19
- package/dist/jsx/formEngine/useSessionRoles.mjs.map +1 -1
- package/dist/jsx/formEngine/useSessionRoles.native.js +53 -19
- package/dist/jsx/formEngine/useSessionRoles.native.js.map +1 -1
- package/dist/jsx/index.js +2 -2
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +2 -2
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +6 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/package.json +16 -16
- package/src/FrappeForm.tsx +94 -62
- package/src/FrappeListView.tsx +12 -7
- package/src/FrappeTable.rights.spec.tsx +154 -0
- package/src/FrappeTable.tsx +21 -2
- package/src/formEngine/docPermissions.spec.ts +145 -0
- package/src/formEngine/docPermissions.ts +128 -0
- package/src/formEngine/index.ts +14 -1
- package/src/formEngine/useDocRights.ts +41 -0
- package/src/formEngine/useSessionRoles.ts +55 -29
- package/src/frappeRoutes.spec.tsx +116 -4
- package/src/index.ts +14 -0
- package/types/FrappeForm.d.ts.map +1 -1
- package/types/FrappeListView.d.ts +3 -2
- package/types/FrappeListView.d.ts.map +1 -1
- package/types/FrappeTable.d.ts +11 -2
- package/types/FrappeTable.d.ts.map +1 -1
- package/types/formEngine/docPermissions.d.ts +54 -0
- package/types/formEngine/docPermissions.d.ts.map +1 -0
- package/types/formEngine/index.d.ts +6 -1
- package/types/formEngine/index.d.ts.map +1 -1
- package/types/formEngine/useDocRights.d.ts +24 -0
- package/types/formEngine/useDocRights.d.ts.map +1 -0
- package/types/formEngine/useSessionRoles.d.ts +12 -2
- package/types/formEngine/useSessionRoles.d.ts.map +1 -1
- package/types/index.d.ts +2 -2
- package/types/index.d.ts.map +1 -1
package/src/FrappeTable.tsx
CHANGED
|
@@ -24,6 +24,8 @@ import { FilterManager, SavedFilterDropdown, useSavedFilters } from "@multiplatf
|
|
|
24
24
|
import { useFrappeConfig } from "./FrappeProvider";
|
|
25
25
|
import { FrappeReconnectIndicator } from "./FrappeReconnectIndicator";
|
|
26
26
|
import { docFieldsToFilterFields } from "./filterFields";
|
|
27
|
+
import type { DocRights } from "./formEngine/docPermissions";
|
|
28
|
+
import { useDocRights } from "./formEngine/useDocRights";
|
|
27
29
|
import { useFrappeRows } from "./useFrappeRows";
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -136,9 +138,17 @@ export interface FrappeTableProps<TDoc extends { name: string; doctype: string }
|
|
|
136
138
|
* meta-derived menu offers the doctype's fields, including ones the
|
|
137
139
|
* server refuses to filter on, so a live app should pass the honoured
|
|
138
140
|
* set by hand. Passing it skips the meta read entirely, which also drops
|
|
139
|
-
* title-field row labels back to the document name
|
|
141
|
+
* title-field row labels back to the document name, unless the table
|
|
142
|
+
* still reads meta for `rights`.
|
|
140
143
|
*/
|
|
141
144
|
filterableFields?: FilterableField[];
|
|
145
|
+
/**
|
|
146
|
+
* The session's rights on the doctype. Without write the cells render
|
|
147
|
+
* read-only, without create there is no add row, without delete no row
|
|
148
|
+
* delete. Omitted, the table resolves them from the doctype's meta and the
|
|
149
|
+
* session's roles; a fixtures connection withholds nothing.
|
|
150
|
+
*/
|
|
151
|
+
rights?: Pick<DocRights, "create" | "write" | "delete">;
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
/**
|
|
@@ -326,6 +336,7 @@ export function FrappeTable<TDoc extends { name: string; doctype: string }>({
|
|
|
326
336
|
paginationMode = "pages",
|
|
327
337
|
loadPage: loadPageProp,
|
|
328
338
|
filterableFields: filterableFieldsProp,
|
|
339
|
+
rights: rightsProp,
|
|
329
340
|
layout,
|
|
330
341
|
...dataTableProps
|
|
331
342
|
}: FrappeTableProps<TDoc>) {
|
|
@@ -424,13 +435,18 @@ export function FrappeTable<TDoc extends { name: string; doctype: string }>({
|
|
|
424
435
|
// too: the read goes to its "DocType" collection with the baseURL a
|
|
425
436
|
// fixture host may still carry withheld, so a corpus without the entry
|
|
426
437
|
// yields no menu instead of dialling getdoctype (MPO-230).
|
|
427
|
-
const skipMeta = !!filterableFieldsProp;
|
|
438
|
+
const skipMeta = !!filterableFieldsProp && (!!rightsProp || !!connectionConfig.fixtures);
|
|
428
439
|
const metaConnection = useMemo(() => {
|
|
429
440
|
if (skipMeta) return undefined;
|
|
430
441
|
if (!connectionConfig.fixtures) return connectionConfig;
|
|
431
442
|
return { ...connectionConfig, baseURL: undefined };
|
|
432
443
|
}, [skipMeta, connectionConfig]);
|
|
433
444
|
const { meta } = useDocTypeMeta(metaConnection, doctype);
|
|
445
|
+
const { rights: resolvedRights } = useDocRights(
|
|
446
|
+
rightsProp ? undefined : connectionConfig,
|
|
447
|
+
rightsProp ? undefined : meta,
|
|
448
|
+
);
|
|
449
|
+
const rights = rightsProp ?? resolvedRights;
|
|
434
450
|
const metaFilterFields = useMemo(
|
|
435
451
|
() => (meta?.fields ? docFieldsToFilterFields(meta.fields) : undefined),
|
|
436
452
|
[meta],
|
|
@@ -867,6 +883,9 @@ export function FrappeTable<TDoc extends { name: string; doctype: string }>({
|
|
|
867
883
|
{...(filterableFields ? { filterableFields } : {})}
|
|
868
884
|
isFetching={isFetching}
|
|
869
885
|
{...dataTableProps}
|
|
886
|
+
readOnly={dataTableProps.readOnly || !rights.write}
|
|
887
|
+
showAddRow={dataTableProps.showAddRow && rights.create}
|
|
888
|
+
showDeleteRow={dataTableProps.showDeleteRow && rights.delete}
|
|
870
889
|
{...(layout ? { layout } : {})}
|
|
871
890
|
/>
|
|
872
891
|
</YStack>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The session's doc-level rights from getdoctype's DocPerm rows and the
|
|
2
|
+
// session roles (MPO-390), against frappe/permissions.py
|
|
3
|
+
// get_role_permissions: permlevel 0 only, OR across the user's roles,
|
|
4
|
+
// if_owner rows scoped to the user's own records except create.
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { allDocRights, resolveDocRights, sessionDocRights, type DocRights } from "./docPermissions";
|
|
7
|
+
import type { DocPermissionRule } from "./doctypeMeta";
|
|
8
|
+
|
|
9
|
+
const pokeMove: DocPermissionRule[] = [
|
|
10
|
+
{ role: "System Manager", read: 1, write: 1, create: 1, delete: 1, export: 1 },
|
|
11
|
+
{ role: "Pokemon Editor", read: 1, write: 1, create: 1, delete: 1 },
|
|
12
|
+
{ role: "Guest", read: 1 },
|
|
13
|
+
{ role: "Move Author", read: 1, write: 1, create: 1, delete: 1, if_owner: 1 },
|
|
14
|
+
{ role: "Move Curator", permlevel: 1, read: 1, write: 1 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function granted(rights: DocRights): string[] {
|
|
18
|
+
return Object.entries(rights)
|
|
19
|
+
.filter(([, value]) => value)
|
|
20
|
+
.map(([right]) => right)
|
|
21
|
+
.sort();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("resolveDocRights", () => {
|
|
25
|
+
it("grants a reader only what the Guest row grants", () => {
|
|
26
|
+
const rights = resolveDocRights({ permissions: pokeMove, roles: ["Guest", "All"] });
|
|
27
|
+
expect(granted(rights)).toEqual(["read"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("grants create, write and delete through Pokemon Editor", () => {
|
|
31
|
+
const rights = resolveDocRights({
|
|
32
|
+
permissions: pokeMove,
|
|
33
|
+
roles: ["Pokemon Editor", "Guest", "All"],
|
|
34
|
+
user: "editor@pokemon.test",
|
|
35
|
+
owner: "Administrator",
|
|
36
|
+
});
|
|
37
|
+
expect(rights.create).toBe(true);
|
|
38
|
+
expect(rights.write).toBe(true);
|
|
39
|
+
expect(rights.delete).toBe(true);
|
|
40
|
+
expect(rights.export).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("ORs the rights of every role the user holds", () => {
|
|
44
|
+
const rights = resolveDocRights({
|
|
45
|
+
permissions: pokeMove,
|
|
46
|
+
roles: ["Pokemon Editor", "System Manager"],
|
|
47
|
+
});
|
|
48
|
+
expect(rights.export).toBe(true);
|
|
49
|
+
expect(rights.delete).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("ignores rows above permlevel 0", () => {
|
|
53
|
+
const rights = resolveDocRights({ permissions: pokeMove, roles: ["Move Curator"] });
|
|
54
|
+
expect(granted(rights)).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("scopes if_owner write and delete to the user's own records but keeps create", () => {
|
|
58
|
+
const base = { permissions: pokeMove, roles: ["Move Author"], user: "author@pokemon.test" };
|
|
59
|
+
const own = resolveDocRights({ ...base, owner: "author@pokemon.test" });
|
|
60
|
+
expect([own.create, own.read, own.write, own.delete]).toEqual([true, true, true, true]);
|
|
61
|
+
|
|
62
|
+
const others = resolveDocRights({ ...base, owner: "Administrator" });
|
|
63
|
+
expect([others.create, others.read, others.write, others.delete]).toEqual([
|
|
64
|
+
true,
|
|
65
|
+
false,
|
|
66
|
+
false,
|
|
67
|
+
false,
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const doctype = resolveDocRights(base);
|
|
71
|
+
expect([doctype.create, doctype.read, doctype.write, doctype.delete]).toEqual([
|
|
72
|
+
true,
|
|
73
|
+
true,
|
|
74
|
+
false,
|
|
75
|
+
false,
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("lets a plain row lift the if_owner scope for the same right", () => {
|
|
80
|
+
const rights = resolveDocRights({
|
|
81
|
+
permissions: pokeMove,
|
|
82
|
+
roles: ["Move Author", "Pokemon Editor"],
|
|
83
|
+
user: "author@pokemon.test",
|
|
84
|
+
owner: "Administrator",
|
|
85
|
+
});
|
|
86
|
+
expect(rights.write).toBe(true);
|
|
87
|
+
expect(rights.delete).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("gives Administrator every right, rows or not", () => {
|
|
91
|
+
expect(resolveDocRights({ permissions: [], roles: [], user: "Administrator" })).toEqual(
|
|
92
|
+
allDocRights(true),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("adds read and write from a share for the user or for everyone, never delete", () => {
|
|
97
|
+
const reader = {
|
|
98
|
+
permissions: pokeMove,
|
|
99
|
+
roles: ["Guest"],
|
|
100
|
+
user: "reader@pokemon.test",
|
|
101
|
+
owner: "Administrator",
|
|
102
|
+
};
|
|
103
|
+
const shared = resolveDocRights({
|
|
104
|
+
...reader,
|
|
105
|
+
shares: [{ user: "reader@pokemon.test", read: 1, write: 1 }],
|
|
106
|
+
});
|
|
107
|
+
expect([shared.write, shared.delete]).toEqual([true, false]);
|
|
108
|
+
|
|
109
|
+
const everyone = resolveDocRights({ ...reader, shares: [{ everyone: 1, write: 1 }] });
|
|
110
|
+
expect(everyone.write).toBe(true);
|
|
111
|
+
|
|
112
|
+
const someoneElse = resolveDocRights({
|
|
113
|
+
...reader,
|
|
114
|
+
shares: [{ user: "other@pokemon.test", write: 1 }],
|
|
115
|
+
});
|
|
116
|
+
expect(someoneElse.write).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("sessionDocRights", () => {
|
|
121
|
+
const meta = { permissions: pokeMove };
|
|
122
|
+
|
|
123
|
+
it("withholds nothing on a fixtures connection", () => {
|
|
124
|
+
expect(sessionDocRights({ fixtures: true, meta: undefined, roles: undefined })).toEqual(
|
|
125
|
+
allDocRights(true),
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("grants nothing on a live connection without meta (Guest's getdoctype 403)", () => {
|
|
130
|
+
expect(sessionDocRights({ fixtures: false, meta: undefined, roles: ["Guest"] })).toEqual(
|
|
131
|
+
allDocRights(false),
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("grants nothing on a live connection whose roles are loading or unreadable", () => {
|
|
136
|
+
expect(sessionDocRights({ fixtures: false, meta, roles: undefined })).toEqual(
|
|
137
|
+
allDocRights(false),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("resolves the session's roles once both are known", () => {
|
|
142
|
+
const rights = sessionDocRights({ fixtures: false, meta, roles: ["Pokemon Editor"] });
|
|
143
|
+
expect([rights.create, rights.write, rights.delete]).toEqual([true, true, true]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doc-level rights for the session, the permlevel-0 half of desk's
|
|
3
|
+
* permission model (fieldPermissions.ts is the field-level half).
|
|
4
|
+
*
|
|
5
|
+
* Server parity with frappe/permissions.py `get_role_permissions`: a right is
|
|
6
|
+
* granted when a permlevel-0 DocPerm row of one of the session's roles grants
|
|
7
|
+
* it. When every granting row is `if_owner`, the right holds only on records
|
|
8
|
+
* the session user owns, except `create`, which an `if_owner` row still
|
|
9
|
+
* grants, and `read` / `select` at doctype level, which stay open so the list
|
|
10
|
+
* can be read (the server narrows it to the user's own rows). Administrator
|
|
11
|
+
* holds every right, as the server's `allow_everything()` does. A record's
|
|
12
|
+
* DocShare rows for the user or for everyone add read, write and share, as
|
|
13
|
+
* desk perm.js applies `docinfo.shared`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { DocPermissionRule, DoctypeMeta } from "./doctypeMeta";
|
|
17
|
+
|
|
18
|
+
export const docRightNames = [
|
|
19
|
+
"select",
|
|
20
|
+
"read",
|
|
21
|
+
"write",
|
|
22
|
+
"create",
|
|
23
|
+
"delete",
|
|
24
|
+
"submit",
|
|
25
|
+
"cancel",
|
|
26
|
+
"amend",
|
|
27
|
+
"print",
|
|
28
|
+
"email",
|
|
29
|
+
"report",
|
|
30
|
+
"import",
|
|
31
|
+
"export",
|
|
32
|
+
"share",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export type DocRight = (typeof docRightNames)[number];
|
|
36
|
+
|
|
37
|
+
export type DocRights = Record<DocRight, boolean>;
|
|
38
|
+
|
|
39
|
+
/** A DocShare row as the live collection or docinfo carries it. */
|
|
40
|
+
export interface DocShareGrant {
|
|
41
|
+
user?: string | null;
|
|
42
|
+
everyone?: number | boolean | null;
|
|
43
|
+
read?: number | boolean | null;
|
|
44
|
+
write?: number | boolean | null;
|
|
45
|
+
share?: number | boolean | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ResolveDocRightsInput {
|
|
49
|
+
permissions: readonly DocPermissionRule[] | undefined;
|
|
50
|
+
roles: readonly string[];
|
|
51
|
+
user?: string;
|
|
52
|
+
/** The record's owner. Omitted, the rights are the doctype's (a list, a new record). */
|
|
53
|
+
owner?: string;
|
|
54
|
+
shares?: readonly DocShareGrant[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function allDocRights(granted: boolean): DocRights {
|
|
58
|
+
const rights = {} as DocRights;
|
|
59
|
+
for (const right of docRightNames) rights[right] = granted;
|
|
60
|
+
return rights;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const openAtDoctypeLevel = new Set<DocRight>(["read", "select"]);
|
|
64
|
+
|
|
65
|
+
export function resolveDocRights({
|
|
66
|
+
permissions,
|
|
67
|
+
roles,
|
|
68
|
+
user,
|
|
69
|
+
owner,
|
|
70
|
+
shares,
|
|
71
|
+
}: ResolveDocRightsInput): DocRights {
|
|
72
|
+
if (user === "Administrator") return allDocRights(true);
|
|
73
|
+
const roleSet = new Set(roles);
|
|
74
|
+
const rows = (permissions ?? []).filter(
|
|
75
|
+
(row) => !!row.role && roleSet.has(row.role) && !(Number(row.permlevel ?? 0) || 0),
|
|
76
|
+
);
|
|
77
|
+
const isOwner = owner !== undefined && user !== undefined && owner === user;
|
|
78
|
+
const rights = allDocRights(false);
|
|
79
|
+
for (const right of docRightNames) {
|
|
80
|
+
const granting = rows.filter((row) => !!row[right]);
|
|
81
|
+
if (granting.length === 0) continue;
|
|
82
|
+
const ownerOnly = granting.every((row) => !!row.if_owner);
|
|
83
|
+
rights[right] =
|
|
84
|
+
!ownerOnly ||
|
|
85
|
+
right === "create" ||
|
|
86
|
+
isOwner ||
|
|
87
|
+
(owner === undefined && openAtDoctypeLevel.has(right));
|
|
88
|
+
}
|
|
89
|
+
if (user && owner !== undefined) {
|
|
90
|
+
for (const share of shares ?? []) {
|
|
91
|
+
if (share.user !== user && !share.everyone) continue;
|
|
92
|
+
if (share.read) rights.read = true;
|
|
93
|
+
if (share.write) rights.write = true;
|
|
94
|
+
if (share.share) rights.share = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return rights;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface SessionDocRightsInput {
|
|
101
|
+
/** A fixtures-only connection: no session exists, so nothing is withheld. */
|
|
102
|
+
fixtures: boolean;
|
|
103
|
+
meta: Pick<DoctypeMeta, "permissions"> | undefined;
|
|
104
|
+
/** The session's roles; undefined while they load or when they can't be read. */
|
|
105
|
+
roles: readonly string[] | undefined;
|
|
106
|
+
user?: string;
|
|
107
|
+
owner?: string;
|
|
108
|
+
shares?: readonly DocShareGrant[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The rights a surface offers. A fixtures connection keeps every action; a
|
|
113
|
+
* live one grants nothing until both the doctype's meta and the session's
|
|
114
|
+
* roles are known, so a reader who can't load meta (Guest) or whose roles
|
|
115
|
+
* can't be read is shown a read-only surface.
|
|
116
|
+
*/
|
|
117
|
+
export function sessionDocRights({
|
|
118
|
+
fixtures,
|
|
119
|
+
meta,
|
|
120
|
+
roles,
|
|
121
|
+
user,
|
|
122
|
+
owner,
|
|
123
|
+
shares,
|
|
124
|
+
}: SessionDocRightsInput): DocRights {
|
|
125
|
+
if (fixtures) return allDocRights(true);
|
|
126
|
+
if (!meta || !roles) return allDocRights(false);
|
|
127
|
+
return resolveDocRights({ permissions: meta.permissions, roles, user, owner, shares });
|
|
128
|
+
}
|
package/src/formEngine/index.ts
CHANGED
|
@@ -32,4 +32,17 @@ export type { BuildTabsOptions, FormTab } from "./formLayout";
|
|
|
32
32
|
export { useFormEngine } from "./useFormEngine";
|
|
33
33
|
export type { FormEngine } from "./useFormEngine";
|
|
34
34
|
|
|
35
|
-
export { useSessionRoles } from "./useSessionRoles";
|
|
35
|
+
export { useFrappeSession, useSessionRoles } from "./useSessionRoles";
|
|
36
|
+
export type { FrappeSession } from "./useSessionRoles";
|
|
37
|
+
|
|
38
|
+
export { allDocRights, docRightNames, resolveDocRights, sessionDocRights } from "./docPermissions";
|
|
39
|
+
export type {
|
|
40
|
+
DocRight,
|
|
41
|
+
DocRights,
|
|
42
|
+
DocShareGrant,
|
|
43
|
+
ResolveDocRightsInput,
|
|
44
|
+
SessionDocRightsInput,
|
|
45
|
+
} from "./docPermissions";
|
|
46
|
+
|
|
47
|
+
export { useDocRights } from "./useDocRights";
|
|
48
|
+
export type { DocRightsOptions, DocRightsResult } from "./useDocRights";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { sessionDocRights, type DocRights, type DocShareGrant } from "./docPermissions";
|
|
3
|
+
import type { DoctypeMeta } from "./doctypeMeta";
|
|
4
|
+
import { useFrappeSession, type SessionConnection } from "./useSessionRoles";
|
|
5
|
+
|
|
6
|
+
export interface DocRightsOptions {
|
|
7
|
+
/** Roles to resolve against instead of the session's. */
|
|
8
|
+
roles?: string[];
|
|
9
|
+
/** The record's owner; omitted for the doctype's rights. */
|
|
10
|
+
owner?: string;
|
|
11
|
+
shares?: readonly DocShareGrant[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DocRightsResult {
|
|
15
|
+
rights: DocRights;
|
|
16
|
+
/** The session's roles are still loading, so `rights` withholds what they may grant. */
|
|
17
|
+
pending: boolean;
|
|
18
|
+
user?: string;
|
|
19
|
+
roles?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The session's rights on a doctype, or on one record of it, from the
|
|
24
|
+
* doctype's DocPerm rows (`meta.permissions`) and the session's roles.
|
|
25
|
+
* See `sessionDocRights` for the fixtures and unknown-session rules.
|
|
26
|
+
*/
|
|
27
|
+
export function useDocRights(
|
|
28
|
+
connection: SessionConnection | undefined,
|
|
29
|
+
meta: DoctypeMeta | undefined,
|
|
30
|
+
{ roles: rolesOverride, owner, shares }: DocRightsOptions = {},
|
|
31
|
+
): DocRightsResult {
|
|
32
|
+
const fixtures = Boolean(connection?.fixtures);
|
|
33
|
+
const session = useFrappeSession(meta && !fixtures ? connection : undefined);
|
|
34
|
+
const roles = rolesOverride ?? session.roles;
|
|
35
|
+
const rights = useMemo(
|
|
36
|
+
() => sessionDocRights({ fixtures, meta, roles, user: session.user, owner, shares }),
|
|
37
|
+
[fixtures, meta, roles, session.user, owner, shares],
|
|
38
|
+
);
|
|
39
|
+
const pending = !fixtures && Boolean(meta) && !rolesOverride && session.state === "loading";
|
|
40
|
+
return { rights, pending, user: session.user, roles };
|
|
41
|
+
}
|
|
@@ -13,61 +13,87 @@
|
|
|
13
13
|
import { useEffect, useRef, useState } from "react";
|
|
14
14
|
import { createHttpClient, type FrappeConnection } from "./formActions";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
interface SessionIdentity {
|
|
17
|
+
user: string;
|
|
18
|
+
roles: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const sessionCache = new Map<string, Promise<SessionIdentity>>();
|
|
17
22
|
|
|
18
|
-
function
|
|
23
|
+
function fetchSession(connection: FrappeConnection): Promise<SessionIdentity> {
|
|
19
24
|
const http = createHttpClient(connection);
|
|
20
|
-
return http
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return list;
|
|
34
|
-
});
|
|
25
|
+
return http.get<{ message?: string }>("/api/method/frappe.auth.get_logged_user").then((res) => {
|
|
26
|
+
const user = res.payload?.message;
|
|
27
|
+
if (!user || typeof user !== "string") throw new Error("No session user");
|
|
28
|
+
return http
|
|
29
|
+
.get<{ message?: string[] }>("/api/method/frappe.core.doctype.user.user.get_roles", {
|
|
30
|
+
uid: user,
|
|
31
|
+
})
|
|
32
|
+
.then((rolesRes) => {
|
|
33
|
+
const roles = rolesRes.payload?.message;
|
|
34
|
+
if (!Array.isArray(roles)) throw new Error("No roles returned");
|
|
35
|
+
return { user, roles };
|
|
36
|
+
});
|
|
37
|
+
});
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/** A connection that may not name a bench yet (a provider's config before it resolves). */
|
|
38
|
-
type SessionConnection = Omit<FrappeConnection, "baseURL"> & { baseURL?: string };
|
|
41
|
+
export type SessionConnection = Omit<FrappeConnection, "baseURL"> & { baseURL?: string };
|
|
39
42
|
|
|
40
|
-
export
|
|
43
|
+
export interface FrappeSession {
|
|
44
|
+
user?: string;
|
|
45
|
+
roles?: string[];
|
|
46
|
+
/**
|
|
47
|
+
* `idle`: no bench to ask (no connection, a fixtures-only one, or none
|
|
48
|
+
* given). `loading`: asked, not answered. `ready`: user and roles known.
|
|
49
|
+
* `failed`: the bench answered without a session (Guest) or refused.
|
|
50
|
+
*/
|
|
51
|
+
state: "idle" | "loading" | "ready" | "failed";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const idleSession: FrappeSession = { state: "idle" };
|
|
55
|
+
const loadingSession: FrappeSession = { state: "loading" };
|
|
56
|
+
|
|
57
|
+
export function useFrappeSession(connection: SessionConnection | undefined): FrappeSession {
|
|
41
58
|
const baseURL = connection?.baseURL;
|
|
42
59
|
const fixtures = connection?.fixtures;
|
|
43
|
-
const
|
|
60
|
+
const active = Boolean(connection) && !fixtures && Boolean(baseURL);
|
|
61
|
+
const [session, setSession] = useState<FrappeSession>(() =>
|
|
62
|
+
active ? { state: "loading" } : idleSession,
|
|
63
|
+
);
|
|
44
64
|
const connectionRef = useRef(connection);
|
|
45
65
|
connectionRef.current = connection;
|
|
46
66
|
|
|
47
67
|
useEffect(() => {
|
|
48
68
|
const current = connectionRef.current;
|
|
49
69
|
if (!current || fixtures || !baseURL) {
|
|
50
|
-
|
|
70
|
+
setSession(idleSession);
|
|
51
71
|
return;
|
|
52
72
|
}
|
|
53
73
|
let cancelled = false;
|
|
54
|
-
|
|
74
|
+
setSession((prev) => (prev.state === "loading" ? prev : { state: "loading" }));
|
|
75
|
+
let promise = sessionCache.get(baseURL);
|
|
55
76
|
if (!promise) {
|
|
56
|
-
promise =
|
|
57
|
-
|
|
58
|
-
promise.catch(() =>
|
|
77
|
+
promise = fetchSession({ ...current, baseURL });
|
|
78
|
+
sessionCache.set(baseURL, promise);
|
|
79
|
+
promise.catch(() => sessionCache.delete(baseURL));
|
|
59
80
|
}
|
|
60
81
|
promise
|
|
61
|
-
.then((
|
|
62
|
-
if (!cancelled)
|
|
82
|
+
.then(({ user, roles }) => {
|
|
83
|
+
if (!cancelled) setSession({ user, roles, state: "ready" });
|
|
63
84
|
})
|
|
64
85
|
.catch(() => {
|
|
65
|
-
if (!cancelled)
|
|
86
|
+
if (!cancelled) setSession({ state: "failed" });
|
|
66
87
|
});
|
|
67
88
|
return () => {
|
|
68
89
|
cancelled = true;
|
|
69
90
|
};
|
|
70
91
|
}, [baseURL, fixtures]);
|
|
71
92
|
|
|
72
|
-
return
|
|
93
|
+
if (!active) return idleSession;
|
|
94
|
+
return session.state === "idle" ? loadingSession : session;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function useSessionRoles(connection: SessionConnection | undefined): string[] | undefined {
|
|
98
|
+
return useFrappeSession(connection).roles;
|
|
73
99
|
}
|