@protobase/ui 0.65.1 → 0.66.1
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/app/import/csv-chunks.js +70 -0
- package/dist/app/import/line-breaks.js +47 -0
- package/dist/app/import/sheet-file.js +1 -1
- package/dist/app/incoming-card.js +2 -2
- package/dist/app/incoming-relations.js +4 -4
- package/dist/app/record-lines.js +3 -3
- package/dist/app/record-page.js +14 -11
- package/dist/app/use-lines.js +6 -6
- package/dist/app/use-record-lines.js +38 -16
- package/dist/locale/en.js +0 -2
- package/dist/locale/nl.js +0 -2
- package/dist/package.js +1 -1
- package/dist/shell/assets/{app-XyM0p_d6.js → app-D-elg6qm.js} +7 -7
- package/dist/shell/assets/{calls-DJW8sAaA.js → calls-DHvl29Pd.js} +2 -2
- package/dist/shell/assets/export-worker-Bux-6dp9.js +15 -0
- package/dist/shell/assets/{index-DMYBOMYl.js → index-D8SK4U2s.js} +2 -2
- package/dist/shell/assets/{maplibre-CRmpQfDl.js → maplibre-sUHB6Qig.js} +2 -2
- package/dist/shell/assets/parse-worker-CbZA-uNr.js +35 -0
- package/dist/shell/assets/preload-helper-BRlYOzPJ.js +2 -0
- package/dist/shell/assets/{record-map-DsP4sXes.js → record-map-CZ__Yamf.js} +2 -2
- package/dist/shell/assets/{records-map-BXh3aP0f.js → records-map-hxWNR_fi.js} +2 -2
- package/dist/shell/assets/{shared-protobase-ui-Bt5-cYRJ.js → shared-protobase-ui-7FTH3m59.js} +2 -2
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +2 -2
- package/dist/shell/protobase.sw.js +2 -2
- package/dist/shell-source-maps/assets/app-D-elg6qm.js.map +1 -0
- package/dist/shell-source-maps/assets/{calls-DJW8sAaA.js.map → calls-DHvl29Pd.js.map} +1 -1
- package/dist/shell-source-maps/assets/export-worker-Bux-6dp9.js.map +1 -0
- package/dist/shell-source-maps/assets/{index-DMYBOMYl.js.map → index-D8SK4U2s.js.map} +1 -1
- package/dist/shell-source-maps/assets/{maplibre-CRmpQfDl.js.map → maplibre-sUHB6Qig.js.map} +1 -1
- package/dist/shell-source-maps/assets/parse-worker-CbZA-uNr.js.map +1 -0
- package/dist/shell-source-maps/assets/preload-helper-BRlYOzPJ.js.map +1 -0
- package/dist/shell-source-maps/assets/{record-map-DsP4sXes.js.map → record-map-CZ__Yamf.js.map} +1 -1
- package/dist/shell-source-maps/assets/{records-map-BXh3aP0f.js.map → records-map-hxWNR_fi.js.map} +1 -1
- package/dist/shell-source-maps/assets/{shared-protobase-ui-Bt5-cYRJ.js.map → shared-protobase-ui-7FTH3m59.js.map} +1 -1
- package/package.json +8 -4
- package/dist/app/import/csv-parser.js +0 -119
- package/dist/shell/assets/export-worker-ClXdfeuq.js +0 -15
- package/dist/shell/assets/parse-worker-DxsmAjef.js +0 -18
- package/dist/shell/assets/preload-helper-BgLOXTzV.js +0 -2
- package/dist/shell-source-maps/assets/app-XyM0p_d6.js.map +0 -1
- package/dist/shell-source-maps/assets/export-worker-ClXdfeuq.js.map +0 -1
- package/dist/shell-source-maps/assets/parse-worker-DxsmAjef.js.map +0 -1
- package/dist/shell-source-maps/assets/preload-helper-BgLOXTzV.js.map +0 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createLineBreakNormalizer } from "./line-breaks.js";
|
|
2
|
+
import Papa from "papaparse";
|
|
3
|
+
//#region src/app/import/csv-chunks.ts
|
|
4
|
+
const headerEnd = (text) => {
|
|
5
|
+
let quoted = false;
|
|
6
|
+
for (let index = 0; index < text.length; index++) {
|
|
7
|
+
const char = text[index];
|
|
8
|
+
if (char === "\"") quoted = !quoted;
|
|
9
|
+
else if (!quoted && (char === "\n" || char === "\r")) return index;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const headerDelimiter = (header) => Papa.parse(header, { delimitersToGuess: [
|
|
13
|
+
",",
|
|
14
|
+
";",
|
|
15
|
+
" "
|
|
16
|
+
] }).meta.delimiter;
|
|
17
|
+
/**
|
|
18
|
+
* A CSV parser fed text a chunk at a time, so a large file is read without holding it whole: papaparse's (RFC 4180,
|
|
19
|
+
* quoted fields with `""` for a quote and line breaks inside), with the header line's delimiter and rows ended by CRLF,
|
|
20
|
+
* LF or CR, mixed if need be. `push` returns the rows completed by the chunk, `end` the last one; the line break ending
|
|
21
|
+
* the file makes no row of its own. A byte order mark at the start is dropped.
|
|
22
|
+
*/
|
|
23
|
+
const createCsvParser = () => {
|
|
24
|
+
let pending = "";
|
|
25
|
+
let started;
|
|
26
|
+
const start = (end) => {
|
|
27
|
+
const delimiter = headerDelimiter(pending.slice(0, end));
|
|
28
|
+
const normalizer = createLineBreakNormalizer(delimiter);
|
|
29
|
+
started = {
|
|
30
|
+
parser: new Papa.Parser({
|
|
31
|
+
delimiter,
|
|
32
|
+
newline: "\n"
|
|
33
|
+
}),
|
|
34
|
+
normalizer
|
|
35
|
+
};
|
|
36
|
+
pending = normalizer.push(pending);
|
|
37
|
+
return started;
|
|
38
|
+
};
|
|
39
|
+
const completed = (parser) => {
|
|
40
|
+
const result = parser.parse(pending, 0, true);
|
|
41
|
+
pending = pending.slice(result.meta.cursor);
|
|
42
|
+
return result.data;
|
|
43
|
+
};
|
|
44
|
+
const push = (chunk) => {
|
|
45
|
+
if (started) {
|
|
46
|
+
pending += started.normalizer.push(chunk);
|
|
47
|
+
return completed(started.parser);
|
|
48
|
+
}
|
|
49
|
+
pending += chunk;
|
|
50
|
+
if (pending.charCodeAt(0) === 65279) pending = pending.slice(1);
|
|
51
|
+
const end = headerEnd(pending);
|
|
52
|
+
if (end === void 0) return [];
|
|
53
|
+
return completed(start(end).parser);
|
|
54
|
+
};
|
|
55
|
+
const end = () => {
|
|
56
|
+
if (!started && pending === "") return [];
|
|
57
|
+
const { parser } = started ?? start(headerEnd(pending) ?? pending.length);
|
|
58
|
+
const rows = completed(parser);
|
|
59
|
+
if (pending === "") return rows;
|
|
60
|
+
const last = parser.parse(pending, 0, false).data;
|
|
61
|
+
pending = "";
|
|
62
|
+
return [...rows, ...last];
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
push,
|
|
66
|
+
end
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
70
|
+
export { createCsvParser };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/app/import/line-breaks.ts
|
|
2
|
+
/**
|
|
3
|
+
* Turns every CR and CRLF between rows into an LF, a chunk of CSV text at a time, so a file whose rows end in mixed
|
|
4
|
+
* line breaks reads as one: papaparse takes a single line break per file. A quote opens a quoted field only at a
|
|
5
|
+
* field's start, as papaparse reads it, and line breaks inside quoted fields are cell text and stay as they are.
|
|
6
|
+
*/
|
|
7
|
+
const createLineBreakNormalizer = (delimiter) => {
|
|
8
|
+
let state = "start";
|
|
9
|
+
let crSeen = false;
|
|
10
|
+
const push = (chunk) => {
|
|
11
|
+
let out = "";
|
|
12
|
+
for (const char of chunk) {
|
|
13
|
+
if (crSeen) {
|
|
14
|
+
crSeen = false;
|
|
15
|
+
if (char === "\n") continue;
|
|
16
|
+
}
|
|
17
|
+
if (state === "quoted") {
|
|
18
|
+
if (char === "\"") state = "quoteSeen";
|
|
19
|
+
out += char;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (state === "quoteSeen") {
|
|
23
|
+
state = "quoted";
|
|
24
|
+
if (char === "\"") {
|
|
25
|
+
out += char;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
state = "unquoted";
|
|
29
|
+
}
|
|
30
|
+
if (char === "\r" || char === "\n") {
|
|
31
|
+
crSeen = char === "\r";
|
|
32
|
+
state = "start";
|
|
33
|
+
out += "\n";
|
|
34
|
+
} else if (char === "\"" && state === "start") {
|
|
35
|
+
state = "quoted";
|
|
36
|
+
out += char;
|
|
37
|
+
} else {
|
|
38
|
+
state = char === delimiter ? "start" : "unquoted";
|
|
39
|
+
out += char;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
};
|
|
44
|
+
return { push };
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { createLineBreakNormalizer };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { text } from "./messages.js";
|
|
2
|
-
import { createCsvParser } from "./csv-
|
|
2
|
+
import { createCsvParser } from "./csv-chunks.js";
|
|
3
3
|
import { openWorkbook } from "./xlsx-reader.js";
|
|
4
4
|
//#region src/app/import/sheet-file.ts
|
|
5
5
|
const startsAsZip = async (file) => {
|
|
@@ -10,9 +10,9 @@ import { ChevronRight } from "lucide-react";
|
|
|
10
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
11
|
//#region src/app/incoming-card.tsx
|
|
12
12
|
/** The rail's card of the records pointing at this one: per resource and field, how many, linking to them in their list. */
|
|
13
|
-
const IncomingCard = ({ model, recordKey, layout
|
|
13
|
+
const IncomingCard = ({ model, recordKey, layout }) => {
|
|
14
14
|
const { resources, views } = useAdminMeta();
|
|
15
|
-
const counts = useIncomingCounts(incomingRelations(model, resources, views, layout
|
|
15
|
+
const counts = useIncomingCounts(incomingRelations(model, resources, views, layout), resources, recordKey);
|
|
16
16
|
if (counts.length === 0) return null;
|
|
17
17
|
return /* @__PURE__ */ jsx(SidebarCard, {
|
|
18
18
|
title: text("relatedRecords"),
|
|
@@ -6,13 +6,13 @@ const where$1 = createWhere();
|
|
|
6
6
|
/**
|
|
7
7
|
* The relations that point at records of `model`, as the rail lists them by default: each filterable relation of
|
|
8
8
|
* another resource the user can see (a hidden resource or field is not in the meta), to a record of one key column.
|
|
9
|
-
* Left out are a resource's tenant, which every record holds,
|
|
10
|
-
*
|
|
9
|
+
* Left out are a resource's tenant, which every record holds, and what an `l.related` section of the page already
|
|
10
|
+
* lists, the record's lines included. Two fields of one resource are told apart by their labels.
|
|
11
11
|
*/
|
|
12
|
-
const incomingRelations = (model, resources, views, layout
|
|
12
|
+
const incomingRelations = (model, resources, views, layout) => {
|
|
13
13
|
if (model.primaryKey.length !== 1) return [];
|
|
14
14
|
const listed = new Set(layoutItems(layout).flatMap((item) => item.kind === "related" && !item.through && !item.from ? [`${item.resource}.${item.field}`] : []));
|
|
15
|
-
const found = Object.values(resources).flatMap((source) => Object.values(source.fields).filter((field) => field.type === "relation" && field.relation?.resource === model.name && field.relation.columns.length === 1 && field.filterable).filter((field) => field.name !== source.tenant &&
|
|
15
|
+
const found = Object.values(resources).flatMap((source) => Object.values(source.fields).filter((field) => field.type === "relation" && field.relation?.resource === model.name && field.relation.columns.length === 1 && field.filterable).filter((field) => field.name !== source.tenant && !listed.has(`${source.name}.${field.name}`)).map((field) => ({
|
|
16
16
|
source,
|
|
17
17
|
field: field.name
|
|
18
18
|
})));
|
package/dist/app/record-lines.js
CHANGED
|
@@ -20,7 +20,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
20
20
|
*/
|
|
21
21
|
const RecordLines = ({ lines, editable, saving }) => {
|
|
22
22
|
const { resources, views } = useAdminMeta();
|
|
23
|
-
const { model, columns, data, draft } = lines;
|
|
23
|
+
const { item, model, columns, data, draft } = lines;
|
|
24
24
|
const permissions = usePermissions(model.name);
|
|
25
25
|
const view = views[model.name];
|
|
26
26
|
const [picked, setPicked] = useState({});
|
|
@@ -89,8 +89,8 @@ const RecordLines = ({ lines, editable, saving }) => {
|
|
|
89
89
|
return rejection.message;
|
|
90
90
|
};
|
|
91
91
|
return /* @__PURE__ */ jsxs(Section, {
|
|
92
|
-
title:
|
|
93
|
-
help:
|
|
92
|
+
title: item.title,
|
|
93
|
+
help: item.help,
|
|
94
94
|
children: [!data.complete && /* @__PURE__ */ jsx("p", {
|
|
95
95
|
className: "mb-2 text-muted-foreground",
|
|
96
96
|
children: text("tooManyLines", { limit: formatInt(1e3) })
|
package/dist/app/record-page.js
CHANGED
|
@@ -412,6 +412,18 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
412
412
|
children
|
|
413
413
|
}, `group:${item.title}`)];
|
|
414
414
|
}
|
|
415
|
+
if (item.kind === "related" && item.lines) {
|
|
416
|
+
if (!lines.model || !lines.item || preview) return [];
|
|
417
|
+
return [/* @__PURE__ */ jsx(RecordLines, {
|
|
418
|
+
lines: {
|
|
419
|
+
...lines,
|
|
420
|
+
model: lines.model,
|
|
421
|
+
item: lines.item
|
|
422
|
+
},
|
|
423
|
+
editable: shown === "form",
|
|
424
|
+
saving: editor.saving
|
|
425
|
+
}, `lines:${item.title}`)];
|
|
426
|
+
}
|
|
415
427
|
if (item.kind === "related") {
|
|
416
428
|
if (item.from !== void 0 && !known(item.from)) return [];
|
|
417
429
|
return [/* @__PURE__ */ jsx(RelatedSection, {
|
|
@@ -548,15 +560,7 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
548
560
|
className: "divide-y divide-border",
|
|
549
561
|
children: tab.items.flatMap(mainItem)
|
|
550
562
|
}, `tab:${tab.title}`),
|
|
551
|
-
after.flatMap(mainItem)
|
|
552
|
-
lines.model && !preview && /* @__PURE__ */ jsx(RecordLines, {
|
|
553
|
-
lines: {
|
|
554
|
-
...lines,
|
|
555
|
-
model: lines.model
|
|
556
|
-
},
|
|
557
|
-
editable: shown === "form",
|
|
558
|
-
saving: editor.saving
|
|
559
|
-
})
|
|
563
|
+
after.flatMap(mainItem)
|
|
560
564
|
]
|
|
561
565
|
}), /* @__PURE__ */ jsxs("aside", {
|
|
562
566
|
className: "space-y-4 md:sticky md:top-[var(--pb-sticky-top,0px)] md:max-h-[calc(100dvh-var(--pb-sticky-top,0px)-var(--pb-sticky-bottom,0px))] md:self-start md:overflow-y-auto md:pb-4 md:pt-8",
|
|
@@ -601,8 +605,7 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
601
605
|
!preview && /* @__PURE__ */ jsx(IncomingCard, {
|
|
602
606
|
model,
|
|
603
607
|
recordKey,
|
|
604
|
-
layout
|
|
605
|
-
lines: lines.model
|
|
608
|
+
layout
|
|
606
609
|
})
|
|
607
610
|
]
|
|
608
611
|
})]
|
package/dist/app/use-lines.js
CHANGED
|
@@ -3,20 +3,20 @@ import { useEffect, useMemo } from "react";
|
|
|
3
3
|
//#region src/app/use-lines.ts
|
|
4
4
|
/** The most lines a record's line editor loads; a record with more shows them read-only, as it cannot save them as a whole. */
|
|
5
5
|
const linesLimit = 1e3;
|
|
6
|
-
const byPosition = (a, b) => Number(a.position) - Number(b.position);
|
|
7
6
|
/**
|
|
8
|
-
* The numbered lines of a record, in position
|
|
9
|
-
* large as the API gives. `labels` names the records their relations point at. Nothing is
|
|
7
|
+
* The numbered lines of a record, in the order of their `position` field, as the server has them: every page, up to
|
|
8
|
+
* `linesLimit`, each as large as the API gives. `labels` names the records their relations point at. Nothing is
|
|
9
|
+
* fetched without a lines resource.
|
|
10
10
|
*/
|
|
11
|
-
const useLines = (lines, link, parentKey, labels) => {
|
|
11
|
+
const useLines = (lines, link, position, parentKey, labels) => {
|
|
12
12
|
const list = useList(lines?.name ?? "", {
|
|
13
13
|
enabled: Boolean(lines && link),
|
|
14
14
|
filter: link ? `${link.name} = "${parentKey}"` : void 0,
|
|
15
15
|
pageSize: 500,
|
|
16
16
|
labels,
|
|
17
|
-
...lines?.fields
|
|
17
|
+
...position && lines?.fields[position]?.sortable && { orderBy: `${position} asc` }
|
|
18
18
|
});
|
|
19
|
-
const items = useMemo(() => [...list.items].sort(
|
|
19
|
+
const items = useMemo(() => position ? [...list.items].sort((a, b) => Number(a[position]) - Number(b[position])) : list.items, [list.data, position]);
|
|
20
20
|
const more = list.hasNextPage && list.items.length < 1e3;
|
|
21
21
|
useEffect(() => {
|
|
22
22
|
if (more && !list.isFetchingNextPage) list.fetchNextPage();
|
|
@@ -5,42 +5,64 @@ import { listLabels } from "../live/relation-labels.js";
|
|
|
5
5
|
import { useLines } from "./use-lines.js";
|
|
6
6
|
import { useLinesDraft } from "./use-lines-draft.js";
|
|
7
7
|
import { useMemo } from "react";
|
|
8
|
+
import { layoutItems } from "@protobase/schema/internal";
|
|
8
9
|
//#region src/app/use-record-lines.ts
|
|
9
|
-
/**
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The record's lines, as its view declares them with `l.related(..., { lines: true })`: the section, the lines'
|
|
12
|
+
* resource, the relation that points at the record and the field that orders them.
|
|
13
|
+
*/
|
|
14
|
+
const findLinesResource = (resources, view) => {
|
|
15
|
+
const item = layoutItems(view?.layout ?? []).find((each) => each.kind === "related" && each.lines === true);
|
|
16
|
+
if (item?.kind !== "related") return;
|
|
17
|
+
const model = resources[item.resource];
|
|
18
|
+
const link = model?.fields[item.field];
|
|
19
|
+
const position = model?.position?.field;
|
|
20
|
+
if (!model || !link || !position || model.position?.within !== item.field) return;
|
|
21
|
+
return {
|
|
22
|
+
item,
|
|
23
|
+
model,
|
|
24
|
+
link,
|
|
25
|
+
position
|
|
26
|
+
};
|
|
27
|
+
};
|
|
11
28
|
/**
|
|
12
29
|
* The lines' columns: the list columns of their view, or its first fields, without what the editor fills in itself:
|
|
13
30
|
* the key, the tenant, the relation to the record and the position, which is the order of the rows.
|
|
14
31
|
*/
|
|
15
|
-
const lineColumnNames = (lines, view, link) => listColumnNames(lines, view).filter((name) => {
|
|
32
|
+
const lineColumnNames = (lines, view, link, position) => listColumnNames(lines, view).filter((name) => {
|
|
16
33
|
const field = lines.fields[name];
|
|
17
|
-
return field && !field.writeOnly && name !== link && name !==
|
|
34
|
+
return field && !field.writeOnly && name !== link && name !== position && name !== lines.tenant && !lines.primaryKey.includes(name);
|
|
18
35
|
});
|
|
19
|
-
/** The record's lines when
|
|
36
|
+
/** The record's lines when its view declares them: the section, the resource, its columns, the lines as stored and the changes made to them. */
|
|
20
37
|
const useRecordLines = (parent, parentKey) => {
|
|
21
38
|
const { resources, views } = useAdminMeta();
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
const found = useMemo(() => findLinesResource(resources, views[parent.name]), [
|
|
40
|
+
resources,
|
|
41
|
+
views,
|
|
42
|
+
parent
|
|
43
|
+
]);
|
|
44
|
+
const model = found?.model;
|
|
45
|
+
const columns = found ? lineColumnNames(found.model, views[found.model.name], found.link.name, found.position) : [];
|
|
46
|
+
const data = useLines(model, found?.link, found?.position, parentKey, model ? listLabels(resources, views, model, columns) : void 0);
|
|
47
|
+
const target = useMemo(() => found ? {
|
|
48
|
+
model: found.model,
|
|
49
|
+
position: found.position,
|
|
29
50
|
link: {
|
|
30
|
-
field: link.name,
|
|
51
|
+
field: found.link.name,
|
|
31
52
|
value: keyValue(parent, parentKey)
|
|
32
53
|
}
|
|
33
54
|
} : void 0, [
|
|
34
|
-
|
|
35
|
-
link,
|
|
55
|
+
found,
|
|
36
56
|
parent,
|
|
37
57
|
parentKey
|
|
38
58
|
]);
|
|
59
|
+
const draft = useLinesDraft(target, data.items);
|
|
39
60
|
return {
|
|
61
|
+
item: found?.item,
|
|
40
62
|
model,
|
|
41
63
|
columns,
|
|
42
64
|
data,
|
|
43
|
-
draft
|
|
65
|
+
draft
|
|
44
66
|
};
|
|
45
67
|
};
|
|
46
68
|
//#endregion
|
package/dist/locale/en.js
CHANGED
|
@@ -734,8 +734,6 @@ var en_default = {
|
|
|
734
734
|
"nothingYet": "Nothing here yet.",
|
|
735
735
|
"couldNotLoadRelated": "Could not load",
|
|
736
736
|
"relatedRecords": "Related records",
|
|
737
|
-
"lines": "Lines",
|
|
738
|
-
"linesHelp": "What the document bills for. Edit a line in place, add one, or drag the handle to change the order; Save applies it all.",
|
|
739
737
|
"lineNumber": "Line {place}",
|
|
740
738
|
"lineField": "{label}, line {place}",
|
|
741
739
|
"tooManyLines": "This record has more than {limit} lines, so they show here read-only."
|
package/dist/locale/nl.js
CHANGED
|
@@ -734,8 +734,6 @@ var nl_default = {
|
|
|
734
734
|
"nothingYet": "Hier staat nog niets.",
|
|
735
735
|
"couldNotLoadRelated": "Kon niet laden",
|
|
736
736
|
"relatedRecords": "Gerelateerde records",
|
|
737
|
-
"lines": "Regels",
|
|
738
|
-
"linesHelp": "Waar het document voor factureert. Wijzig een regel ter plekke, voeg er een toe, of sleep de greep om de volgorde te wijzigen; Bewaren past alles toe.",
|
|
739
737
|
"lineNumber": "Regel {place}",
|
|
740
738
|
"lineField": "{label}, regel {place}",
|
|
741
739
|
"tooManyLines": "Dit record heeft meer dan {limit} regels, dus ze staan hier alleen ter inzage."
|
package/dist/package.js
CHANGED