@softr/sdk 0.0.74 → 0.0.75-alpha.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/action.d.ts +31 -1
- package/dist/action.js +26 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.js +2 -1
- package/dist/codegen/datasource-types.js +1 -14
- package/dist/comments/comments.d.ts +168 -0
- package/dist/comments/comments.js +413 -0
- package/dist/comments/use-comment-composer.d.ts +46 -0
- package/dist/comments/use-comment-composer.js +134 -0
- package/dist/core/file-upload.d.ts +7 -2
- package/dist/core/file-upload.js +6 -1
- package/dist/core/options.d.ts +17 -2
- package/dist/core/options.js +30 -2
- package/dist/core/use-file-upload.d.ts +2 -2
- package/dist/datasource/datasource.d.ts +2 -1
- package/dist/datasource/datasource.js +3 -2
- package/dist/datasource/fields.d.ts +16 -24
- package/dist/default-error-overlay.d.ts +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/package.js +1 -1
- package/dist/pwa.d.ts +2 -2
- package/dist/server.d.ts +3 -2
- package/dist/server.js +2 -1
- package/package.json +3 -1
- package/skills/softr-sdk/SKILL.md +8 -4
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/comments/SKILL.md +181 -0
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +5 -4
- package/skills/softr-sdk/formatting/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +1 -1
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +2 -2
- package/skills/softr-sdk/record-queries/SKILL.md +7 -1
- package/skills/softr-sdk/record-queries/references/filters-and-sorting.md +1 -1
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { CommentMention } from "./comments.js";
|
|
2
|
+
import { ChangeEvent, RefObject, SyntheticEvent } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/comments/use-comment-composer.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The state of a comment box: the text, the people mentioned in it, and the `@` fragment being
|
|
8
|
+
* typed. Spread `textareaProps` onto a `<textarea>`. Show a mention menu while `query` is not
|
|
9
|
+
* `null`, and call `pick` with the person chosen from it.
|
|
10
|
+
*
|
|
11
|
+
* Post `text` and `mentions` as they are, then call `reset` once the post succeeds.
|
|
12
|
+
*
|
|
13
|
+
* To edit a comment, pass its source as initial, in a composer mounted only while editing.
|
|
14
|
+
* `initial` is read once, on mount.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const composer = useCommentComposer()
|
|
18
|
+
* <textarea {...composer.textareaProps} />
|
|
19
|
+
* {composer.query !== null && <MentionMenu query={composer.query} onPick={composer.pick} />}
|
|
20
|
+
*/
|
|
21
|
+
declare function useCommentComposer(options?: {
|
|
22
|
+
/** Fills the box to edit a comment: pass its `source`. */
|
|
23
|
+
initial?: {
|
|
24
|
+
text: string;
|
|
25
|
+
mentions: ReadonlyArray<CommentMention>;
|
|
26
|
+
};
|
|
27
|
+
}): {
|
|
28
|
+
text: string;
|
|
29
|
+
mentions: Array<CommentMention>;
|
|
30
|
+
/** What follows the `@` before the caret, or `null` when the caret is not in a mention. */
|
|
31
|
+
query: string | null;
|
|
32
|
+
textareaProps: {
|
|
33
|
+
ref: RefObject<HTMLTextAreaElement | null>;
|
|
34
|
+
value: string;
|
|
35
|
+
onChange: (event: ChangeEvent<HTMLTextAreaElement>) => void;
|
|
36
|
+
onSelect: (event: SyntheticEvent<HTMLTextAreaElement>) => void;
|
|
37
|
+
};
|
|
38
|
+
/** Replaces the `@` fragment with the person's `@name`, and puts the caret after it. */
|
|
39
|
+
pick: (person: {
|
|
40
|
+
name: string;
|
|
41
|
+
email: string;
|
|
42
|
+
}) => void;
|
|
43
|
+
reset: () => void;
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { useCommentComposer };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { assert } from "../assertions.js";
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/comments/use-comment-composer.ts
|
|
5
|
+
const EMPTY = {
|
|
6
|
+
text: "",
|
|
7
|
+
mentions: [],
|
|
8
|
+
caret: 0
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The state of a comment box: the text, the people mentioned in it, and the `@` fragment being
|
|
12
|
+
* typed. Spread `textareaProps` onto a `<textarea>`. Show a mention menu while `query` is not
|
|
13
|
+
* `null`, and call `pick` with the person chosen from it.
|
|
14
|
+
*
|
|
15
|
+
* Post `text` and `mentions` as they are, then call `reset` once the post succeeds.
|
|
16
|
+
*
|
|
17
|
+
* To edit a comment, pass its source as initial, in a composer mounted only while editing.
|
|
18
|
+
* `initial` is read once, on mount.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const composer = useCommentComposer()
|
|
22
|
+
* <textarea {...composer.textareaProps} />
|
|
23
|
+
* {composer.query !== null && <MentionMenu query={composer.query} onPick={composer.pick} />}
|
|
24
|
+
*/
|
|
25
|
+
function useCommentComposer(options) {
|
|
26
|
+
const [state, setState] = useState(() => options?.initial === void 0 ? EMPTY : {
|
|
27
|
+
text: options.initial.text,
|
|
28
|
+
mentions: [...options.initial.mentions],
|
|
29
|
+
caret: options.initial.text.length
|
|
30
|
+
});
|
|
31
|
+
const ref = useRef(null);
|
|
32
|
+
const pendingCaret = useRef(null);
|
|
33
|
+
useLayoutEffect(() => {
|
|
34
|
+
const caret = pendingCaret.current;
|
|
35
|
+
if (caret === null || ref.current === null) return;
|
|
36
|
+
pendingCaret.current = null;
|
|
37
|
+
ref.current.focus();
|
|
38
|
+
ref.current.setSelectionRange(caret, caret);
|
|
39
|
+
});
|
|
40
|
+
const fragment = findMentionQuery(state.text, state.caret, state.mentions);
|
|
41
|
+
return {
|
|
42
|
+
text: state.text,
|
|
43
|
+
mentions: state.mentions,
|
|
44
|
+
query: fragment?.query ?? null,
|
|
45
|
+
textareaProps: {
|
|
46
|
+
ref,
|
|
47
|
+
value: state.text,
|
|
48
|
+
onChange(event) {
|
|
49
|
+
const next = event.currentTarget.value;
|
|
50
|
+
const caret = event.currentTarget.selectionStart;
|
|
51
|
+
setState((previous) => ({
|
|
52
|
+
text: next,
|
|
53
|
+
mentions: remapMentions(previous.text, next, previous.mentions),
|
|
54
|
+
caret
|
|
55
|
+
}));
|
|
56
|
+
},
|
|
57
|
+
onSelect(event) {
|
|
58
|
+
const caret = event.currentTarget.selectionStart;
|
|
59
|
+
setState((previous) => previous.caret === caret ? previous : {
|
|
60
|
+
...previous,
|
|
61
|
+
caret
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
pick(person) {
|
|
66
|
+
assert(fragment, "pick needs an @ fragment under the caret");
|
|
67
|
+
const label = `@${person.name}`;
|
|
68
|
+
const text = `${state.text.slice(0, fragment.start)}${label} ${state.text.slice(state.caret)}`;
|
|
69
|
+
const added = {
|
|
70
|
+
...person,
|
|
71
|
+
start: fragment.start,
|
|
72
|
+
end: fragment.start + label.length
|
|
73
|
+
};
|
|
74
|
+
const caret = added.end + 1;
|
|
75
|
+
pendingCaret.current = caret;
|
|
76
|
+
setState({
|
|
77
|
+
text,
|
|
78
|
+
mentions: [...remapMentions(state.text, text, state.mentions), added],
|
|
79
|
+
caret
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
reset() {
|
|
83
|
+
setState(EMPTY);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** A fragment longer than this is prose, not a name being typed. */
|
|
88
|
+
const MENTION_QUERY_MAX_LENGTH = 32;
|
|
89
|
+
/**
|
|
90
|
+
* Moves mention ranges through an edit. A textarea reports only its new value, so the edit is the
|
|
91
|
+
* span between the longest common prefix and suffix. Ranges after it shift. Ranges it reached into
|
|
92
|
+
* are dropped, which is how deleting part of a name un-mentions that person.
|
|
93
|
+
*/
|
|
94
|
+
function remapMentions(previous, next, mentions) {
|
|
95
|
+
if (previous === next) return mentions;
|
|
96
|
+
let prefix = 0;
|
|
97
|
+
while (prefix < previous.length && prefix < next.length && previous[prefix] === next[prefix]) prefix += 1;
|
|
98
|
+
let suffix = 0;
|
|
99
|
+
while (suffix < previous.length - prefix && suffix < next.length - prefix && previous[previous.length - 1 - suffix] === next[next.length - 1 - suffix]) suffix += 1;
|
|
100
|
+
const editStart = prefix;
|
|
101
|
+
const editEnd = previous.length - suffix;
|
|
102
|
+
const delta = next.length - previous.length;
|
|
103
|
+
return mentions.flatMap((mention) => {
|
|
104
|
+
if (mention.end <= editStart) return [mention];
|
|
105
|
+
if (mention.start >= editEnd) return [{
|
|
106
|
+
...mention,
|
|
107
|
+
start: mention.start + delta,
|
|
108
|
+
end: mention.end + delta
|
|
109
|
+
}];
|
|
110
|
+
return [];
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The `@` fragment the caret sits in, or `null`. An `@` does not count when:
|
|
115
|
+
* - it does not start a word, so an email address opens no menu;
|
|
116
|
+
* - it belongs to a mention already made;
|
|
117
|
+
* - the fragment ran past a newline or is long enough to be prose. Spaces are fine, names have them.
|
|
118
|
+
*/
|
|
119
|
+
function findMentionQuery(text, caret, mentions) {
|
|
120
|
+
const before = text.slice(0, caret);
|
|
121
|
+
const at = before.lastIndexOf("@");
|
|
122
|
+
if (at === -1) return null;
|
|
123
|
+
if (at > 0 && !/\s/.test(before.charAt(at - 1))) return null;
|
|
124
|
+
if (mentions.some((mention) => at >= mention.start && at < mention.end)) return null;
|
|
125
|
+
const query = before.slice(at + 1);
|
|
126
|
+
if (query.includes("\n") || query.length > MENTION_QUERY_MAX_LENGTH) return null;
|
|
127
|
+
return {
|
|
128
|
+
query,
|
|
129
|
+
start: at
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
export { useCommentComposer };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DatasourceAction } from "../action.js";
|
|
1
|
+
import { CommentsAction, DatasourceAction } from "../action.js";
|
|
2
2
|
|
|
3
3
|
//#region src/core/file-upload.d.ts
|
|
4
4
|
|
|
@@ -10,7 +10,12 @@ type FileAttachment = {
|
|
|
10
10
|
url: string;
|
|
11
11
|
filename: string;
|
|
12
12
|
id: string;
|
|
13
|
+
type?: string | null;
|
|
13
14
|
};
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* A COMMENTS action uploads a comment attachment.
|
|
17
|
+
* The platform checks the comment settings.
|
|
18
|
+
*/
|
|
19
|
+
declare function upload(action: DatasourceAction | CommentsAction, file: File): Promise<FileAttachment>;
|
|
15
20
|
//#endregion
|
|
16
21
|
export { FileAttachment, upload };
|
package/dist/core/file-upload.js
CHANGED
|
@@ -5,6 +5,10 @@ import { platformApiUrl } from "../auth/platform.js";
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
|
|
7
7
|
//#region src/core/file-upload.ts
|
|
8
|
+
/**
|
|
9
|
+
* A COMMENTS action uploads a comment attachment.
|
|
10
|
+
* The platform checks the comment settings.
|
|
11
|
+
*/
|
|
8
12
|
async function upload(action, file) {
|
|
9
13
|
const { applicationId } = sessionConfig();
|
|
10
14
|
const baseUrl = await platformApiUrl();
|
|
@@ -23,7 +27,8 @@ async function upload(action, file) {
|
|
|
23
27
|
return {
|
|
24
28
|
url: resultJSON.url,
|
|
25
29
|
filename: resultJSON.filename || "Untitled file",
|
|
26
|
-
id: resultJSON.id || randomUUID()
|
|
30
|
+
id: resultJSON.id || randomUUID(),
|
|
31
|
+
type: resultJSON.type ?? null
|
|
27
32
|
};
|
|
28
33
|
}
|
|
29
34
|
|
package/dist/core/options.d.ts
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
import { DatasourceAction, ReadShape } from "../action.js";
|
|
1
|
+
import { ActionShape, DatasourceAction, ReadShape } from "../action.js";
|
|
2
2
|
import "@tanstack/react-start/server-only";
|
|
3
3
|
|
|
4
4
|
//#region src/core/options.d.ts
|
|
5
|
+
/** One choice of a SELECT field, as the field holds it now. */
|
|
6
|
+
type FieldChoice = {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
color: string | null;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The choices a SELECT field holds right now, read from Softr on every call. A choice the
|
|
13
|
+
* builder adds or renames later shows up without a new build. The field must be one the
|
|
14
|
+
* action declares. Any query or mutation over the table works.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const loadStatuses = createServerFn().handler(() => ds.choices(listProjects, 'status'))
|
|
18
|
+
*/
|
|
19
|
+
declare function listChoices<TShape extends ActionShape>(action: DatasourceAction<TShape>, field: keyof TShape["fields"] & string): Promise<Array<FieldChoice>>;
|
|
5
20
|
/** One row a linked-record picker offers. */
|
|
6
21
|
type RecordOption = {
|
|
7
22
|
id: string;
|
|
@@ -33,4 +48,4 @@ declare function listOptions<TShape extends ReadShape>(query: DatasourceAction<T
|
|
|
33
48
|
limit?: number;
|
|
34
49
|
}): Promise<RecordOptions>;
|
|
35
50
|
//#endregion
|
|
36
|
-
export { RecordOption, RecordOptions, listOptions };
|
|
51
|
+
export { FieldChoice, RecordOption, RecordOptions, listChoices, listOptions };
|
package/dist/core/options.js
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
|
+
import { assert } from "../assertions.js";
|
|
2
|
+
import { fetch } from "../fetch.js";
|
|
3
|
+
import { readSessionToken } from "../auth/session.js";
|
|
1
4
|
import { makeChain } from "../datasource/fields.js";
|
|
2
|
-
import { listRecords } from "../datasource/record.js";
|
|
5
|
+
import { describeFailure, dsUrl, listRecords, refererHeader, resolveTarget } from "../datasource/record.js";
|
|
3
6
|
import "@tanstack/react-start/server-only";
|
|
4
7
|
|
|
5
8
|
//#region src/core/options.ts
|
|
6
9
|
/**
|
|
10
|
+
* The choices a SELECT field holds right now, read from Softr on every call. A choice the
|
|
11
|
+
* builder adds or renames later shows up without a new build. The field must be one the
|
|
12
|
+
* action declares. Any query or mutation over the table works.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const loadStatuses = createServerFn().handler(() => ds.choices(listProjects, 'status'))
|
|
16
|
+
*/
|
|
17
|
+
async function listChoices(action, field) {
|
|
18
|
+
const { actionId, select } = resolveTarget(action, action.resource.operation);
|
|
19
|
+
const fieldId = select[field];
|
|
20
|
+
assert(fieldId, () => `action "${actionId}" declares no field "${field}"`);
|
|
21
|
+
const res = await fetch(`${dsUrl(actionId)}/metadata`, { headers: {
|
|
22
|
+
Authorization: `Bearer ${readSessionToken()}`,
|
|
23
|
+
...refererHeader()
|
|
24
|
+
} });
|
|
25
|
+
if (!res.ok) throw new Error(`datasource metadata failed for "${actionId}": ${await describeFailure(res)}`);
|
|
26
|
+
const metadataField = (await res.json()).fields.find(({ id }) => id === fieldId);
|
|
27
|
+
assert(metadataField?.type === "SELECT", () => `"${field}" on action "${actionId}" is not a SELECT field`);
|
|
28
|
+
return (metadataField.options?.choices ?? []).map(({ id, label, color }) => ({
|
|
29
|
+
id,
|
|
30
|
+
label,
|
|
31
|
+
color: color ?? null
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
7
35
|
* The rows a linked field can point at, for a picker: each record of the query as its id and the text
|
|
8
36
|
* of one field, searched on that field when a term is given, a page at a time.
|
|
9
37
|
*
|
|
@@ -36,4 +64,4 @@ function labelOf(value) {
|
|
|
36
64
|
}
|
|
37
65
|
|
|
38
66
|
//#endregion
|
|
39
|
-
export { listOptions };
|
|
67
|
+
export { listChoices, listOptions };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DatasourceAction } from "../action.js";
|
|
1
|
+
import { CommentsAction, DatasourceAction } from "../action.js";
|
|
2
2
|
import { FileAttachment } from "./file-upload.js";
|
|
3
3
|
|
|
4
4
|
//#region src/core/use-file-upload.d.ts
|
|
@@ -16,7 +16,7 @@ type FileItem = {
|
|
|
16
16
|
filename: string;
|
|
17
17
|
error: Error;
|
|
18
18
|
};
|
|
19
|
-
declare function useFileUpload(action: DatasourceAction, options?: {
|
|
19
|
+
declare function useFileUpload(action: DatasourceAction | CommentsAction, options?: {
|
|
20
20
|
initialAttachments?: FileAttachment[];
|
|
21
21
|
}): {
|
|
22
22
|
files: FileItem[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
|
|
2
|
-
import { listOptions } from "../core/options.js";
|
|
2
|
+
import { listChoices, listOptions } from "../core/options.js";
|
|
3
3
|
import { chart, metric } from "./chart.js";
|
|
4
4
|
import "@tanstack/react-start/server-only";
|
|
5
5
|
|
|
@@ -13,6 +13,7 @@ declare const ds: {
|
|
|
13
13
|
metric: typeof metric;
|
|
14
14
|
chart: typeof chart;
|
|
15
15
|
options: typeof listOptions;
|
|
16
|
+
choices: typeof listChoices;
|
|
16
17
|
};
|
|
17
18
|
//#endregion
|
|
18
19
|
export { ds };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
|
|
2
2
|
import { chart, metric } from "./chart.js";
|
|
3
|
-
import { listOptions } from "../core/options.js";
|
|
3
|
+
import { listChoices, listOptions } from "../core/options.js";
|
|
4
4
|
import "@tanstack/react-start/server-only";
|
|
5
5
|
|
|
6
6
|
//#region src/datasource/datasource.ts
|
|
@@ -12,7 +12,8 @@ const ds = {
|
|
|
12
12
|
delete: deleteRecord,
|
|
13
13
|
metric,
|
|
14
14
|
chart,
|
|
15
|
-
options: listOptions
|
|
15
|
+
options: listOptions,
|
|
16
|
+
choices: listChoices
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
//#endregion
|
|
@@ -19,17 +19,21 @@ type NumberChain = EmptyChain & {
|
|
|
19
19
|
lte(value: number): ConstructableFilter;
|
|
20
20
|
between(start: number, end: number): ConstructableFilter;
|
|
21
21
|
};
|
|
22
|
-
type ArrayChain
|
|
23
|
-
is(values: Array<
|
|
24
|
-
isOneOf(values: Array<
|
|
25
|
-
isNoneOf(values: Array<
|
|
26
|
-
hasAllOf(values: Array<
|
|
22
|
+
type ArrayChain = EmptyChain & {
|
|
23
|
+
is(values: Array<string>): ConstructableFilter;
|
|
24
|
+
isOneOf(values: Array<string>): ConstructableFilter;
|
|
25
|
+
isNoneOf(values: Array<string>): ConstructableFilter;
|
|
26
|
+
hasAllOf(values: Array<string>): ConstructableFilter;
|
|
27
27
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Compares choice ids as plain strings. A select's choices change in Studio after the build,
|
|
30
|
+
* and an id read at runtime from ds.choices has to be a valid filter value.
|
|
31
|
+
*/
|
|
32
|
+
type ChoiceChain = EmptyChain & {
|
|
33
|
+
is(value: string): ConstructableFilter;
|
|
34
|
+
isNot(value: string): ConstructableFilter;
|
|
35
|
+
isOneOf(values: Array<string>): ConstructableFilter;
|
|
36
|
+
isNoneOf(values: Array<string>): ConstructableFilter;
|
|
33
37
|
};
|
|
34
38
|
/** What DSv2 filters generically: matched whole or scanned, never against a set. */
|
|
35
39
|
type GenericChain = EmptyChain & {
|
|
@@ -73,32 +77,20 @@ type AnyChain = EmptyChain & {
|
|
|
73
77
|
between(start: number | string, end: number | string): ConstructableFilter;
|
|
74
78
|
isNotBetween(start: number | string, end: number | string): ConstructableFilter;
|
|
75
79
|
};
|
|
76
|
-
type IdOf<TValue> = TValue extends {
|
|
77
|
-
id: infer TId extends string;
|
|
78
|
-
} ? TId : string;
|
|
79
|
-
/**
|
|
80
|
-
* What a choice-shaped field compares against: its own ids where codegen read the choice
|
|
81
|
-
* list, and any string where it did not. A linked record is the open case, and it is the
|
|
82
|
-
* only thing distinguishing one from a select at the type level.
|
|
83
|
-
*/
|
|
84
|
-
type ChoiceIds<TValue> = string extends IdOf<TValue> ? string : IdOf<TValue>;
|
|
85
80
|
type TextFieldType = 'SINGLE_LINE_TEXT' | 'LONG_TEXT' | 'URL' | 'EMAIL' | 'PHONE' | 'ADDRESS' | 'RECORD_ID' | 'LOCATION' | 'TIME' | 'BUTTON';
|
|
86
81
|
type NumberFieldType = 'NUMBER' | 'PERCENT' | 'CURRENCY' | 'PROGRESS' | 'RATING' | 'DURATION' | 'COUNT' | 'AUTONUMBER';
|
|
87
82
|
type DateFieldType = 'DATE' | 'DATETIME' | 'CREATED_AT' | 'UPDATED_AT';
|
|
88
83
|
type ChoiceFieldType = 'SELECT' | 'LINKED_RECORD' | 'CREATED_BY' | 'UPDATED_BY';
|
|
89
84
|
/** The types with no single value to compare: only whether the field holds one. */
|
|
90
85
|
type EmptyFieldType = 'ATTACHMENT' | 'DATE_RANGE';
|
|
91
|
-
type ElementOf<TValue> = TValue extends Array<infer TMember> ? TMember : TValue;
|
|
92
86
|
/**
|
|
93
87
|
* The operators one alias offers, chosen from the type codegen declared for the field rather
|
|
94
88
|
* than from what it reads as: a `DATETIME` and a text field both read as `string`, and only
|
|
95
89
|
* the declared type tells them apart.
|
|
96
90
|
*
|
|
97
|
-
*
|
|
98
|
-
* nowhere else. A field with no declared type lands on the permissive chain rather than on
|
|
99
|
-
* none.
|
|
91
|
+
* A field with no declared type lands on the permissive chain rather than on none.
|
|
100
92
|
*/
|
|
101
|
-
type ChainFor<TFieldType, TValue> = [TFieldType] extends [never] ? AnyChain : TFieldType extends `${infer TBase}[]` ? TBase extends EmptyFieldType ? EmptyChain : ArrayChain
|
|
93
|
+
type ChainFor<TFieldType, TValue> = [TFieldType] extends [never] ? AnyChain : TFieldType extends `${infer TBase}[]` ? TBase extends EmptyFieldType ? EmptyChain : ArrayChain : TFieldType extends TextFieldType ? TextChain<TValue & string> : TFieldType extends NumberFieldType ? NumberChain : TFieldType extends 'USER' ? GenericChain : TFieldType extends 'CHECKBOX' ? BooleanChain : TFieldType extends DateFieldType ? DateChain : TFieldType extends ChoiceFieldType ? ChoiceChain : TFieldType extends EmptyFieldType ? EmptyChain : AnyChain;
|
|
102
94
|
type Combinators = {
|
|
103
95
|
$and(...members: Array<AnyConstructable>): ConstructableFilterGroup;
|
|
104
96
|
$or(...members: Array<AnyConstructable>): ConstructableFilterGroup;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react4 from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/default-error-overlay.d.ts
|
|
4
4
|
type Props = {
|
|
@@ -8,6 +8,6 @@ type Props = {
|
|
|
8
8
|
declare function DefaultErrorOverlay({
|
|
9
9
|
error,
|
|
10
10
|
reset
|
|
11
|
-
}: Props):
|
|
11
|
+
}: Props): react4.JSX.Element;
|
|
12
12
|
//#endregion
|
|
13
13
|
export { DefaultErrorOverlay };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
|
|
2
|
-
import { ActionPermissions, SoftrDatasources, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
2
|
+
import { ActionPermissions, SoftrDatasources, defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
3
|
import { FileAttachment } from "./core/file-upload.js";
|
|
4
4
|
import { FileItem, useFileUpload } from "./core/use-file-upload.js";
|
|
5
|
+
import { CommentAttachmentInput, CommentMention, CommentThread, CommentView } from "./comments/comments.js";
|
|
5
6
|
import { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting } from "./formatting.js";
|
|
6
7
|
import { SoftrContext } from "./auth/context.js";
|
|
7
8
|
import { AuthSettings } from "./auth/platform.js";
|
|
@@ -17,4 +18,4 @@ import { useUserTimezone } from "./timezone.js";
|
|
|
17
18
|
import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
|
|
18
19
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
19
20
|
import { Audience } from "./route/audience.js";
|
|
20
|
-
export { type ActionPermissions, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type User, type WeekDay, assertRouteAccess, checkAccess, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
|
21
|
+
export { type ActionPermissions, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type CommentAttachmentInput, type CommentMention, type CommentThread, type CommentView, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type User, type WeekDay, assertRouteAccess, checkAccess, defineComments, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useFileUpload } from "./core/use-file-upload.js";
|
|
2
|
-
import { defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
2
|
+
import { defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
3
|
import { checkAccess, defineVisibility, isAuthorized } from "./route/visibility.js";
|
|
4
4
|
import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./route/route.js";
|
|
5
5
|
import { definePage, pageHead } from "./route/page.js";
|
|
@@ -14,4 +14,4 @@ import { useUserTimezone } from "./timezone.js";
|
|
|
14
14
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
15
15
|
import { Audience } from "./route/audience.js";
|
|
16
16
|
|
|
17
|
-
export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
|
17
|
+
export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineComments, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
package/dist/package.js
CHANGED
package/dist/pwa.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react5 from "react";
|
|
2
2
|
import "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/pwa.d.ts
|
|
@@ -23,7 +23,7 @@ import "@tanstack/react-start";
|
|
|
23
23
|
* )
|
|
24
24
|
* }
|
|
25
25
|
*/
|
|
26
|
-
declare function SoftrPwaHead():
|
|
26
|
+
declare function SoftrPwaHead(): react5.FunctionComponentElement<react5.FragmentProps>;
|
|
27
27
|
type PwaPlatform = "ios" | "android" | "desktop";
|
|
28
28
|
type PwaInstall = {
|
|
29
29
|
available: false;
|
package/dist/server.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { upload } from "./core/file-upload.js";
|
|
2
|
+
import { comments } from "./comments/comments.js";
|
|
2
3
|
import { auth } from "./auth/server.js";
|
|
3
|
-
import { RecordOption, RecordOptions } from "./core/options.js";
|
|
4
|
+
import { FieldChoice, RecordOption, RecordOptions } from "./core/options.js";
|
|
4
5
|
import { ds } from "./datasource/datasource.js";
|
|
5
6
|
import { workflow } from "./core/workflow.js";
|
|
6
7
|
import { integration } from "./core/integration.js";
|
|
7
8
|
import "@tanstack/react-start/server-only";
|
|
8
|
-
export { type RecordOption, type RecordOptions, auth, ds, integration, upload, workflow };
|
|
9
|
+
export { type FieldChoice, type RecordOption, type RecordOptions, auth, comments, ds, integration, upload, workflow };
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ds } from "./datasource/datasource.js";
|
|
|
3
3
|
import { auth } from "./auth/server.js";
|
|
4
4
|
import { workflow } from "./core/workflow.js";
|
|
5
5
|
import { integration } from "./core/integration.js";
|
|
6
|
+
import { comments } from "./comments/comments.js";
|
|
6
7
|
import "@tanstack/react-start/server-only";
|
|
7
8
|
|
|
8
|
-
export { auth, ds, integration, upload, workflow };
|
|
9
|
+
export { auth, comments, ds, integration, upload, workflow };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softr/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.75-alpha.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"typecheck": "tsc --noEmit"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"marked": "^18.0.14",
|
|
21
|
+
"xss": "^1.0.15",
|
|
20
22
|
"zod": "^4.4.3"
|
|
21
23
|
},
|
|
22
24
|
"devDependencies": {
|
|
@@ -12,7 +12,7 @@ description: >-
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: core
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.75
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
# Softr SDK core
|
|
@@ -30,9 +30,9 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
30
30
|
|
|
31
31
|
| Import | Runs | Holds |
|
|
32
32
|
| --- | --- | --- |
|
|
33
|
-
| `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `formatValue`, `formatField`, `fieldShape`, `registerSoftrFields`, `useOverrides`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
|
|
34
|
-
| `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration` |
|
|
35
|
-
| `@softr/sdk/client` | Browser only |
|
|
33
|
+
| `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineComments`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `formatValue`, `formatField`, `fieldShape`, `registerSoftrFields`, `useOverrides`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
|
|
34
|
+
| `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration`, `comments` |
|
|
35
|
+
| `@softr/sdk/client` | Browser only | `useFileUpload`, `useCommentComposer` |
|
|
36
36
|
| `@softr/sdk/vite` | Build | `softr()`, already in `vite.config.ts` |
|
|
37
37
|
|
|
38
38
|
## Sub-Skills
|
|
@@ -51,6 +51,7 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
51
51
|
| Open the app to AI assistants over MCP | [mcp/SKILL.md](./mcp/SKILL.md) |
|
|
52
52
|
| Let visitors install the app on their home screen | [pwa/SKILL.md](./pwa/SKILL.md) |
|
|
53
53
|
| Show a date, a time or a number | [formatting/SKILL.md](./formatting/SKILL.md) |
|
|
54
|
+
| Add comments to a record | [comments/SKILL.md](./comments/SKILL.md) |
|
|
54
55
|
|
|
55
56
|
## Quick Decision Tree
|
|
56
57
|
|
|
@@ -91,6 +92,9 @@ Adding an install button, or making the app installable?
|
|
|
91
92
|
Showing a date, a time or a number on a page?
|
|
92
93
|
load softr-sdk/formatting
|
|
93
94
|
|
|
95
|
+
Letting visitors comment on a record?
|
|
96
|
+
load softr-sdk/comments
|
|
97
|
+
|
|
94
98
|
Writing a server function, loader, or middleware in general?
|
|
95
99
|
not a Softr concern. Use the TanStack Start skills.
|
|
96
100
|
```
|
|
@@ -18,7 +18,7 @@ description: >-
|
|
|
18
18
|
metadata:
|
|
19
19
|
type: sub-skill
|
|
20
20
|
library: softr-sdk
|
|
21
|
-
library_version: 0.0.
|
|
21
|
+
library_version: 0.0.75
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
**Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
|
|
@@ -12,7 +12,7 @@ description: >-
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: sub-skill
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.75
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
|