attio 0.0.1-experimental.20241112 → 0.0.1-experimental.20241209
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/lib/attio.js +1 -1
- package/lib/build/client/generate-client-entry.js +34 -22
- package/lib/build.js +1 -0
- package/lib/client/bulk-record-action.d.ts +54 -0
- package/lib/client/components/index.d.ts +0 -2
- package/lib/client/hooks/index.d.ts +1 -1
- package/lib/client/hooks/use-workspace.d.ts +13 -0
- package/lib/client/index.d.ts +3 -2
- package/lib/client/record-action.d.ts +53 -0
- package/lib/client/show-dialog.d.ts +33 -0
- package/lib/client/show-toast.d.ts +1 -1
- package/lib/commands/dev.js +2 -0
- package/lib/components/BuildError.js +7 -1
- package/lib/components/CodeGenErrors.js +2 -2
- package/lib/components/Log.js +69 -0
- package/lib/machines/dev-machine.js +6 -3
- package/lib/machines/js-machine.js +0 -8
- package/lib/server/attio-fetch.d.ts +57 -0
- package/lib/server/index.d.ts +1 -0
- package/lib/templates/common/src/assets/icon.png +0 -0
- package/lib/templates/javascript/package.json +5 -3
- package/lib/templates/javascript/src/advice.jsx +16 -0
- package/lib/templates/javascript/src/get-advice.server.js +6 -0
- package/lib/templates/javascript/src/hello-world-action.jsx +17 -0
- package/lib/templates/javascript/src/hello-world-dialog.jsx +25 -0
- package/lib/templates/typescript/package.json +8 -6
- package/lib/templates/typescript/src/advice.tsx +16 -0
- package/lib/templates/typescript/src/get-advice.server.ts +6 -0
- package/lib/templates/typescript/src/hello-world-action.tsx +17 -0
- package/lib/templates/typescript/src/hello-world-dialog.tsx +25 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/util/copy-with-replace.js +8 -3
- package/lib/util/find-surface-exports/find-surface-exports.js +45 -0
- package/lib/util/find-surface-exports/generate-random-file-name.js +13 -0
- package/lib/util/find-surface-exports/parse-file-exports.js +98 -0
- package/lib/util/find-surface-exports/surface-types.js +24 -0
- package/lib/util/find-surface-exports/walk-dir.js +25 -0
- package/lib/util/surfaces.js +4 -0
- package/package.json +3 -3
- package/schema.graphql +14 -0
- package/lib/client/components/action.d.ts +0 -31
- package/lib/client/components/multistep.d.ts +0 -24
- package/lib/client/hooks/use-dialog.d.ts +0 -71
- package/lib/client/register-bulk-record-action.d.ts +0 -36
- package/lib/client/register-record-action.d.ts +0 -36
- package/lib/templates/javascript/src/app.jsx +0 -6
- package/lib/templates/typescript/src/app.tsx +0 -6
package/lib/attio.js
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { findSurfaceExports } from "../../util/find-surface-exports/find-surface-exports.js";
|
|
3
4
|
const ASSET_FILE_EXTENSIONS = ["png"];
|
|
4
|
-
export async function generateClientEntry({ srcDirAbsolute, assetsDirAbsolute,
|
|
5
|
-
|
|
6
|
-
for (const possiblePath of ["tsx", "jsx", "ts", "js"].map((extension) => path.join(srcDirAbsolute, `app.${extension}`))) {
|
|
7
|
-
try {
|
|
8
|
-
await fs.access(possiblePath);
|
|
9
|
-
appFilePathAbsolute = possiblePath;
|
|
10
|
-
break;
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
if (appFilePathAbsolute) {
|
|
16
|
-
onAppFileFound();
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
onAppFileMissing();
|
|
20
|
-
}
|
|
5
|
+
export async function generateClientEntry({ srcDirAbsolute, assetsDirAbsolute, }) {
|
|
6
|
+
const surfaceExports = await findSurfaceExports(srcDirAbsolute);
|
|
21
7
|
let assetFiles;
|
|
22
8
|
try {
|
|
23
9
|
assetFiles = await fs.readdir(assetsDirAbsolute, { recursive: true });
|
|
@@ -31,11 +17,28 @@ export async function generateClientEntry({ srcDirAbsolute, assetsDirAbsolute, o
|
|
|
31
17
|
path: path.join(assetsDirAbsolute, relativeAssetPath),
|
|
32
18
|
name: relativeAssetPath,
|
|
33
19
|
}));
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
20
|
+
const importSurfacesJS = surfaceExports
|
|
21
|
+
.map(([filePath, actionKinds], index) => `import {${actionKinds
|
|
22
|
+
.map((actionKind) => `${actionKind} as ${actionKind}${index}`)
|
|
23
|
+
.join(", ")}} from ${JSON.stringify(filePath)};`)
|
|
24
|
+
.join("\n");
|
|
25
|
+
const surfaceImportNamesBySurfaceType = {
|
|
26
|
+
recordAction: [],
|
|
27
|
+
bulkRecordAction: [],
|
|
28
|
+
};
|
|
29
|
+
surfaceExports.forEach(([, surfaceNames], index) => {
|
|
30
|
+
surfaceNames.forEach((surfaceName) => {
|
|
31
|
+
surfaceImportNamesBySurfaceType[surfaceName].push(`${surfaceName}${index}`);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
const registerSurfacesJS = `registerSurfaces({
|
|
35
|
+
"record-action": [${surfaceImportNamesBySurfaceType.recordAction.join(", ")}],
|
|
36
|
+
"bulk-record-action": [${surfaceImportNamesBySurfaceType.bulkRecordAction.join(", ")}],
|
|
37
|
+
});`;
|
|
38
|
+
const importAssetsJS = assets
|
|
39
|
+
.map((asset, index) => `import A${index} from ${JSON.stringify(asset.path)};`)
|
|
40
|
+
.join("\n");
|
|
41
|
+
const registerAssetsJS = `
|
|
39
42
|
const assets = [];
|
|
40
43
|
|
|
41
44
|
${assets
|
|
@@ -43,5 +46,14 @@ const assets = [];
|
|
|
43
46
|
.join("\n")}
|
|
44
47
|
|
|
45
48
|
registerAssets(assets);
|
|
49
|
+
`;
|
|
50
|
+
return `
|
|
51
|
+
${importSurfacesJS}
|
|
52
|
+
|
|
53
|
+
${importAssetsJS}
|
|
54
|
+
|
|
55
|
+
${registerSurfacesJS}
|
|
56
|
+
|
|
57
|
+
${registerAssetsJS}
|
|
46
58
|
`;
|
|
47
59
|
}
|
package/lib/build.js
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Icon } from "./icon";
|
|
2
|
+
import { ObjectSlug } from "./object-slug";
|
|
3
|
+
/**
|
|
4
|
+
* A bulk record action is some action that can be taken on a group of records,
|
|
5
|
+
*
|
|
6
|
+
* You should use this in a TypeScript file in your app's `src` directory.
|
|
7
|
+
*
|
|
8
|
+
* ## EXAMPLE USAGE
|
|
9
|
+
*
|
|
10
|
+
* ----
|
|
11
|
+
* ```tsx
|
|
12
|
+
* // my-bulk-record-action.tsx
|
|
13
|
+
* import type { BulkRecordAction } from "attio/client";
|
|
14
|
+
*
|
|
15
|
+
* export const bulkRecordAction: BulkRecordAction = {
|
|
16
|
+
* id: "send-invoice",
|
|
17
|
+
* onTrigger: (records) => {
|
|
18
|
+
* console.log("send invoice", records)
|
|
19
|
+
* },
|
|
20
|
+
* label: "Send Invoice",
|
|
21
|
+
* icon: "money-bag"
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
* ----
|
|
25
|
+
*/
|
|
26
|
+
export interface BulkRecordAction {
|
|
27
|
+
/**
|
|
28
|
+
* A unique identifier for the action.
|
|
29
|
+
*/
|
|
30
|
+
readonly id: string;
|
|
31
|
+
/**
|
|
32
|
+
* A function to execute when the action is triggered
|
|
33
|
+
*/
|
|
34
|
+
readonly onTrigger: (records: {
|
|
35
|
+
recordIds: Array<string>;
|
|
36
|
+
object: ObjectSlug;
|
|
37
|
+
}) => Promise<void> | void;
|
|
38
|
+
/**
|
|
39
|
+
* The label to display for the action
|
|
40
|
+
*/
|
|
41
|
+
readonly label: string;
|
|
42
|
+
/**
|
|
43
|
+
* An icon to display in the action, either an `AttioIcon` or
|
|
44
|
+
* a string `.png` referencing a file in your app's `assets`
|
|
45
|
+
* directory.
|
|
46
|
+
*
|
|
47
|
+
* If no `icon` prop is provided, it will default to your app's icon.
|
|
48
|
+
*/
|
|
49
|
+
readonly icon?: Icon;
|
|
50
|
+
/**
|
|
51
|
+
* If provided then the action will only be shown on records of the specified object(s).
|
|
52
|
+
*/
|
|
53
|
+
readonly objects?: ObjectSlug | Array<ObjectSlug>;
|
|
54
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
export { Action } from "./action.js";
|
|
2
1
|
export { ComboboxOption, ComboboxOptionsProvider } from "./combobox.js";
|
|
3
2
|
export { Json } from "./json.js";
|
|
4
|
-
export { Multistep, Step } from "./multistep.js";
|
|
5
3
|
export { Section } from "./section.js";
|
|
6
4
|
export { TextBlock } from "./text-block.js";
|
|
7
5
|
export { Row } from "./row.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { useAsyncCache, AsyncCacheConfig, AsyncFunction } from "./use-async-cache.js";
|
|
2
|
-
export { useDialog } from "./use-dialog.js";
|
|
3
2
|
export { useForm, FormApi, FormSchema } from "./use-form.js";
|
|
4
3
|
export { useQuery } from "./use-query.js";
|
|
5
4
|
export { useRecord } from "./use-record.js";
|
|
6
5
|
export { useRecords } from "./use-records.js";
|
|
6
|
+
export { useWorkspace } from "./use-workspace.js";
|
package/lib/client/index.d.ts
CHANGED
|
@@ -9,9 +9,10 @@ export { FormString } from "./forms/string.js";
|
|
|
9
9
|
export { FormValue } from "./forms/value.js";
|
|
10
10
|
export * from "./forms/path.js";
|
|
11
11
|
export { Icon, AttioIcon } from "./icon.js";
|
|
12
|
-
export { registerRecordAction } from "./register-record-action.js";
|
|
13
|
-
export { registerBulkRecordAction } from "./register-bulk-record-action.js";
|
|
14
12
|
export { showToast, ToastOptions, ToastVariant } from "./show-toast.js";
|
|
15
13
|
export { confirm, ConfirmOptions } from "./confirm.js";
|
|
16
14
|
export { alert, AlertOptions } from "./alert.js";
|
|
17
15
|
export { platform } from "./platform.js";
|
|
16
|
+
export { showDialog, type Dialog } from "./show-dialog.js";
|
|
17
|
+
export type { RecordAction } from "./record-action.js";
|
|
18
|
+
export type { BulkRecordAction } from "./bulk-record-action.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Icon } from "./icon";
|
|
2
|
+
import { ObjectSlug } from "./object-slug";
|
|
3
|
+
/**
|
|
4
|
+
* A record action is some action that can be taken on a record,
|
|
5
|
+
*
|
|
6
|
+
* You should use this in a TypeScript file in your app's `src` directory.
|
|
7
|
+
*
|
|
8
|
+
* ## EXAMPLE USAGE
|
|
9
|
+
*
|
|
10
|
+
* ----
|
|
11
|
+
* ```tsx
|
|
12
|
+
* // my-action.tsx
|
|
13
|
+
* import type { RecordAction } from "attio/client";
|
|
14
|
+
*
|
|
15
|
+
* export const recordAction: RecordAction = {
|
|
16
|
+
* onTrigger: ({recordId, object}) => {
|
|
17
|
+
* console.log("send invoice", recordId, object)
|
|
18
|
+
* },
|
|
19
|
+
* label: "Send Invoice",
|
|
20
|
+
* icon: "money-bag"
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
* ----
|
|
24
|
+
*/
|
|
25
|
+
export interface RecordAction {
|
|
26
|
+
/**
|
|
27
|
+
* A unique identifier for the action.
|
|
28
|
+
*/
|
|
29
|
+
readonly id: string;
|
|
30
|
+
/**
|
|
31
|
+
* A function to execute when the action is triggered
|
|
32
|
+
*/
|
|
33
|
+
readonly onTrigger: (record: {
|
|
34
|
+
recordId: string;
|
|
35
|
+
object: ObjectSlug;
|
|
36
|
+
}) => Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* The label to display for the action
|
|
39
|
+
*/
|
|
40
|
+
readonly label: string;
|
|
41
|
+
/**
|
|
42
|
+
* An icon to display in the action, either an `AttioIcon` or
|
|
43
|
+
* a string `.png` referencing a file in your app’s `assets`
|
|
44
|
+
* directory.
|
|
45
|
+
*
|
|
46
|
+
* If no `icon` prop is provided, it will default to your app’s icon.
|
|
47
|
+
*/
|
|
48
|
+
readonly icon?: Icon;
|
|
49
|
+
/**
|
|
50
|
+
* If provided then the action will only be shown on records of the specified object(s).
|
|
51
|
+
*/
|
|
52
|
+
readonly objects?: ObjectSlug | Array<ObjectSlug>;
|
|
53
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export type Dialog = React.FC<{
|
|
3
|
+
hideDialog: () => void;
|
|
4
|
+
}>;
|
|
5
|
+
interface DialogOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The title of the toast.
|
|
8
|
+
*/
|
|
9
|
+
title: string;
|
|
10
|
+
/**
|
|
11
|
+
* The contents of the dialog.
|
|
12
|
+
*/
|
|
13
|
+
Dialog: Dialog;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Opens a modal dialog.
|
|
17
|
+
*
|
|
18
|
+
* ## EXAMPLE USAGE
|
|
19
|
+
*
|
|
20
|
+
* ----
|
|
21
|
+
* ```tsx
|
|
22
|
+
* // show-dialog.tsx
|
|
23
|
+
* import { showDialog } from "attio/client";
|
|
24
|
+
*
|
|
25
|
+
* await showDialog({
|
|
26
|
+
* title: "Invoice sent",
|
|
27
|
+
* contents: ({hideDialog}) => <TextBlock>Hello world</TextBlock>,
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
* ----
|
|
31
|
+
*/
|
|
32
|
+
export declare function showDialog(options: DialogOptions): Promise<void>;
|
|
33
|
+
export {};
|
package/lib/commands/dev.js
CHANGED
|
@@ -12,6 +12,7 @@ import { renderTypeScriptErrors } from "../components/TypeScriptErrors.js";
|
|
|
12
12
|
import { useFullScreen } from "../hooks/useFullScreen.js";
|
|
13
13
|
import { useTerminalTitle } from "../hooks/useTerminalTitle.js";
|
|
14
14
|
import { devMachine } from "../machines/dev-machine.js";
|
|
15
|
+
import { Log } from "../components/Log.js";
|
|
15
16
|
export const description = "Develop your Attio extension";
|
|
16
17
|
const PrettyDate = ({ date }) => React.createElement(Text, null, format(date, "yyyy-MM-dd HH:mm:ss"));
|
|
17
18
|
export const options = z.object({
|
|
@@ -75,6 +76,7 @@ export default function Dev({ options: { debug } }) {
|
|
|
75
76
|
" ",
|
|
76
77
|
React.createElement(PrettyDate, { date: snapshot.context.lastSuccessfulJavaScriptBuild }),
|
|
77
78
|
"."))),
|
|
79
|
+
React.createElement(Log, null),
|
|
78
80
|
jsError && jsTime && (React.createElement(Box, null,
|
|
79
81
|
React.createElement(Text, null,
|
|
80
82
|
"\u274C Last failed build was at ",
|
|
@@ -31,7 +31,13 @@ const Error = React.forwardRef(({ message, level }, ref) => (React.createElement
|
|
|
31
31
|
message.location.lineText.trim().length -
|
|
32
32
|
message.location.lineText.length +
|
|
33
33
|
1 },
|
|
34
|
-
React.createElement(Text, { color: "greenBright" }, "^")))))
|
|
34
|
+
React.createElement(Text, { color: "greenBright" }, "^"))))),
|
|
35
|
+
message.location.additionalLines &&
|
|
36
|
+
message.location.additionalLines.slice(0, 2).map((line, index) => (React.createElement(Box, { key: `additional-line-${index}`, marginLeft: 4 },
|
|
37
|
+
React.createElement(Text, null,
|
|
38
|
+
message.location.line + index + 1,
|
|
39
|
+
" | ",
|
|
40
|
+
line))))))));
|
|
35
41
|
export function renderBuildErrors({ errors, warnings }) {
|
|
36
42
|
return [
|
|
37
43
|
...(errors ?? []).map((error, index) => (React.createElement(Error, { key: `BuildError-${index}`, message: error, level: "error" }))),
|
|
@@ -11,12 +11,12 @@ export const CodeGenError = React.forwardRef(({ error }, ref) => {
|
|
|
11
11
|
" "),
|
|
12
12
|
" ",
|
|
13
13
|
React.createElement(Text, null, error.message))),
|
|
14
|
-
React.createElement(Box, { paddingTop: 1, flexDirection: "column" },
|
|
14
|
+
error.locations && (React.createElement(Box, { paddingTop: 1, flexDirection: "column" },
|
|
15
15
|
React.createElement(Box, null,
|
|
16
16
|
React.createElement(Text, null, codeFrameColumns(error.source, {
|
|
17
17
|
start: {
|
|
18
18
|
line: error.locations[0].line,
|
|
19
19
|
column: error.locations[0].column,
|
|
20
20
|
},
|
|
21
|
-
}))))));
|
|
21
|
+
})))))));
|
|
22
22
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import React from "react";
|
|
3
|
+
const emojis = {
|
|
4
|
+
log: "📝",
|
|
5
|
+
info: "💬",
|
|
6
|
+
warn: "🔸",
|
|
7
|
+
error: "🚨",
|
|
8
|
+
};
|
|
9
|
+
class GlobalLog {
|
|
10
|
+
constructor() {
|
|
11
|
+
this._messages = [];
|
|
12
|
+
this._listeners = new Map();
|
|
13
|
+
this._nextListenerId = 0;
|
|
14
|
+
}
|
|
15
|
+
notifyListeners() {
|
|
16
|
+
this._listeners.forEach((listener) => listener());
|
|
17
|
+
}
|
|
18
|
+
addMessage(level, messages) {
|
|
19
|
+
this._messages.push({
|
|
20
|
+
level,
|
|
21
|
+
message: messages
|
|
22
|
+
.map((message) => typeof message === "string"
|
|
23
|
+
? message
|
|
24
|
+
: typeof message === "number"
|
|
25
|
+
? message.toString()
|
|
26
|
+
: JSON.stringify(message, null, 2))
|
|
27
|
+
.join(" "),
|
|
28
|
+
});
|
|
29
|
+
this.notifyListeners();
|
|
30
|
+
}
|
|
31
|
+
log(...messages) {
|
|
32
|
+
this.addMessage("log", messages);
|
|
33
|
+
}
|
|
34
|
+
info(...messages) {
|
|
35
|
+
this.addMessage("info", messages);
|
|
36
|
+
}
|
|
37
|
+
warn(...messages) {
|
|
38
|
+
this.addMessage("warn", messages);
|
|
39
|
+
}
|
|
40
|
+
error(...messages) {
|
|
41
|
+
this.addMessage("error", messages);
|
|
42
|
+
}
|
|
43
|
+
clear() {
|
|
44
|
+
this._messages = [];
|
|
45
|
+
this.notifyListeners();
|
|
46
|
+
}
|
|
47
|
+
getMessages() {
|
|
48
|
+
return this._messages;
|
|
49
|
+
}
|
|
50
|
+
listen(listener) {
|
|
51
|
+
const id = this._nextListenerId++;
|
|
52
|
+
this._listeners.set(id, listener);
|
|
53
|
+
return () => this._listeners.delete(id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const globalLog = new GlobalLog();
|
|
57
|
+
export const log = (...messages) => globalLog.log(...messages);
|
|
58
|
+
export const info = (...messages) => globalLog.info(...messages);
|
|
59
|
+
export const warn = (...messages) => globalLog.warn(...messages);
|
|
60
|
+
export const error = (...messages) => globalLog.error(...messages);
|
|
61
|
+
export const Log = () => {
|
|
62
|
+
const messages = React.useSyncExternalStore(() => globalLog.listen(() => {
|
|
63
|
+
}), () => globalLog.getMessages(), () => []);
|
|
64
|
+
if (messages.length === 0)
|
|
65
|
+
return null;
|
|
66
|
+
return (React.createElement(Box, { flexDirection: "column", paddingY: 1 }, messages.map((message, index) => (React.createElement(Box, { key: index, flexDirection: "row", gap: 1 },
|
|
67
|
+
React.createElement(Text, null, emojis[message.level]),
|
|
68
|
+
React.createElement(Text, null, message.message))))));
|
|
69
|
+
};
|
|
@@ -13,6 +13,7 @@ import { codeGenMachine } from "./code-gen-machine.js";
|
|
|
13
13
|
import { envMachine } from "./env-machine.js";
|
|
14
14
|
import { jsMachine } from "./js-machine.js";
|
|
15
15
|
import { tsMachine } from "./ts-machine.js";
|
|
16
|
+
import { error as logError } from "../components/Log.js";
|
|
16
17
|
export const devMachine = setup({
|
|
17
18
|
types: {
|
|
18
19
|
context: {},
|
|
@@ -93,7 +94,7 @@ export const devMachine = setup({
|
|
|
93
94
|
body: clientBundle,
|
|
94
95
|
headers: {
|
|
95
96
|
"Content-Type": "text/javascript",
|
|
96
|
-
"Content-Length": String(clientBundle.length),
|
|
97
|
+
"Content-Length": String(Buffer.from(clientBundle).length),
|
|
97
98
|
},
|
|
98
99
|
}),
|
|
99
100
|
fetch(server_bundle_upload_url, {
|
|
@@ -101,10 +102,12 @@ export const devMachine = setup({
|
|
|
101
102
|
body: serverBundle,
|
|
102
103
|
headers: {
|
|
103
104
|
"Content-Type": "text/javascript",
|
|
104
|
-
"Content-Length": String(serverBundle.length),
|
|
105
|
+
"Content-Length": String(Buffer.from(serverBundle).length),
|
|
105
106
|
},
|
|
106
107
|
}),
|
|
107
|
-
])
|
|
108
|
+
]).catch((error) => {
|
|
109
|
+
logError("Upload Error", error);
|
|
110
|
+
});
|
|
108
111
|
await completeBundleUpload({
|
|
109
112
|
token,
|
|
110
113
|
developerSlug,
|
|
@@ -25,9 +25,6 @@ export const jsMachine = setup({
|
|
|
25
25
|
const assetsDir = path.join(srcDir, "assets");
|
|
26
26
|
const webhooksDir = path.join(srcDir, "webhooks");
|
|
27
27
|
const eventsDir = path.join(srcDir, "events");
|
|
28
|
-
const log = (message) => {
|
|
29
|
-
sendBack({ type: "Log", message });
|
|
30
|
-
};
|
|
31
28
|
buildContexts = (await Promise.all([
|
|
32
29
|
tmp.file({ postfix: ".js" }).then(async (tempFile) => {
|
|
33
30
|
let lastJS;
|
|
@@ -35,10 +32,6 @@ export const jsMachine = setup({
|
|
|
35
32
|
const js = await generateClientEntry({
|
|
36
33
|
srcDirAbsolute: path.resolve(srcDir),
|
|
37
34
|
assetsDirAbsolute: path.resolve(assetsDir),
|
|
38
|
-
onAppFileMissing: () => {
|
|
39
|
-
},
|
|
40
|
-
onAppFileFound: () => {
|
|
41
|
-
},
|
|
42
35
|
});
|
|
43
36
|
if (js === lastJS) {
|
|
44
37
|
return;
|
|
@@ -75,7 +68,6 @@ export const jsMachine = setup({
|
|
|
75
68
|
srcDirAbsolute: path.resolve(srcDir),
|
|
76
69
|
webhooksDirAbsolute: path.resolve(webhooksDir),
|
|
77
70
|
eventDirAbsolute: path.resolve(eventsDir),
|
|
78
|
-
log,
|
|
79
71
|
});
|
|
80
72
|
if (js === lastJS) {
|
|
81
73
|
return;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
type Path = "/comments" | `/comments/${string}` | "/notes" | `/notes/${string}` | "/tasks" | `/tasks/${string}` | "/lists" | `/lists/${string}` | "/self" | "/objects" | "/objects/people/records" | "/objects/people/records/query" | `/objects/people/records/${string}` | "/objects/companies/records" | "/objects/companies/records/query" | `/objects/companies/records/${string}` | "/objects/users/records" | "/objects/users/records/query" | `/objects/users/records/${string}` | "/objects/deals/records" | "/objects/deals/records/query" | `/objects/deals/records/${string}` | "/objects/workspaces/records" | "/objects/workspaces/records/query" | `/objects/workspaces/records/${string}` | `/objects/${string & {
|
|
2
|
+
_brand?: "ObjectPath";
|
|
3
|
+
}}` | "/threads" | `/threads/${string}` | "/webhooks" | `/webhooks/${string}` | "/workspace_members" | `/workspace_members/${string}` | (`/${string & {
|
|
4
|
+
length: number;
|
|
5
|
+
}}${string}` & {
|
|
6
|
+
_brand?: "Path";
|
|
7
|
+
});
|
|
8
|
+
interface FetchAttioOptions {
|
|
9
|
+
/**
|
|
10
|
+
* The HTTP method to use.
|
|
11
|
+
*/
|
|
12
|
+
method: "GET" | "POST" | "DELETE" | "PATCH";
|
|
13
|
+
/**
|
|
14
|
+
* The path to the resource to fetch.
|
|
15
|
+
*/
|
|
16
|
+
path: Path;
|
|
17
|
+
/**
|
|
18
|
+
* The query parameters to use in the request.
|
|
19
|
+
*/
|
|
20
|
+
queryParams?: Record<string, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* The body to use in the request.
|
|
23
|
+
*
|
|
24
|
+
* In the Attio API, this is always an object with the single key "data".
|
|
25
|
+
*/
|
|
26
|
+
body?: {
|
|
27
|
+
data: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Communicates with the Attio API.
|
|
32
|
+
*
|
|
33
|
+
* See: https://developers.attio.com/reference
|
|
34
|
+
*
|
|
35
|
+
* ## EXAMPLE USAGE
|
|
36
|
+
*
|
|
37
|
+
* ----
|
|
38
|
+
* ```tsx
|
|
39
|
+
* import {attioFetch} from "attio/server"
|
|
40
|
+
*
|
|
41
|
+
* const notes = await attioFetch({
|
|
42
|
+
* method: "GET",
|
|
43
|
+
* path: "/notes",
|
|
44
|
+
* })
|
|
45
|
+
* ```
|
|
46
|
+
* ----
|
|
47
|
+
*
|
|
48
|
+
* ## RETURN VALUE
|
|
49
|
+
*
|
|
50
|
+
* The return value is the JSON response.
|
|
51
|
+
*
|
|
52
|
+
* @throws If the request is not successful.
|
|
53
|
+
*/
|
|
54
|
+
export declare function attioFetch(options: FetchAttioOptions): Promise<{
|
|
55
|
+
data: Record<string, unknown> | Array<Record<string, unknown>>;
|
|
56
|
+
}>;
|
|
57
|
+
export {};
|
package/lib/server/index.d.ts
CHANGED
|
Binary file
|
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
"attio": "attio",
|
|
8
8
|
"dev": "attio dev",
|
|
9
9
|
"build": "attio build",
|
|
10
|
+
"format": "yarn run -T prettier --ignore-path $(yarn run -T find-up .prettierignore) --check ./",
|
|
11
|
+
"format-fix": "yarn run format --write ./",
|
|
10
12
|
"clean": "rm -rf dist",
|
|
11
|
-
"lint": "eslint 'src/**/*.{js,jsx}'"
|
|
13
|
+
"lint": "ATTIO_LINT_MODE='exhaustive' eslint 'src/**/*.{js,jsx}'"
|
|
12
14
|
},
|
|
13
15
|
"devDependencies": {
|
|
14
|
-
"eslint": "^8.57.
|
|
15
|
-
"eslint-plugin-react": "^7.
|
|
16
|
+
"eslint": "^8.57.1",
|
|
17
|
+
"eslint-plugin-react": "^7.37.2",
|
|
16
18
|
"eslint-plugin-react-hooks": "^4.6.2"
|
|
17
19
|
},
|
|
18
20
|
"dependencies": {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import {TextBlock, useAsyncCache} from "attio/client"
|
|
3
|
+
import getAdvice from "./get-advice.server"
|
|
4
|
+
|
|
5
|
+
export const Advice = ({recordId}) => {
|
|
6
|
+
// By passing in the recordId, the result will be cached for each recordId
|
|
7
|
+
const {
|
|
8
|
+
values: {advice},
|
|
9
|
+
// ^^^^^^– this key matches
|
|
10
|
+
// vvvvvv– this key
|
|
11
|
+
} = useAsyncCache({advice: [getAdvice, recordId]})
|
|
12
|
+
// ^^^^^^^^^ ^^^^^^^^
|
|
13
|
+
// async fn parameter(s)
|
|
14
|
+
|
|
15
|
+
return <TextBlock align="center">{`"${advice}"`}</TextBlock>
|
|
16
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export default async function getAdvice(recordId) {
|
|
2
|
+
// We don't really need the recordId for this API, but this is how we could use a parameter
|
|
3
|
+
const response = await fetch(`https://api.adviceslip.com/advice?${recordId}`)
|
|
4
|
+
const data = await response.json()
|
|
5
|
+
return data.slip.advice
|
|
6
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { showDialog } from "attio/client"
|
|
3
|
+
import { HelloWorldDialog } from "./hello-world-dialog"
|
|
4
|
+
|
|
5
|
+
export const recordAction = {
|
|
6
|
+
id: "slug-to-be-replaced",
|
|
7
|
+
label: "title-to-be-replaced",
|
|
8
|
+
onTrigger: async ({recordId}) => {
|
|
9
|
+
showDialog({
|
|
10
|
+
title: "title-to-be-replaced",
|
|
11
|
+
Dialog: () => {
|
|
12
|
+
// This is a React component. It can use hooks and render other components.
|
|
13
|
+
return <HelloWorldDialog recordId={recordId} />
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
},
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import {TextBlock} from "attio/client"
|
|
3
|
+
import {Advice} from "./advice"
|
|
4
|
+
|
|
5
|
+
const Loading = () => <TextBlock>Loading advice...</TextBlock>
|
|
6
|
+
|
|
7
|
+
export function HelloWorldDialog ({recordId}) {
|
|
8
|
+
const [seconds, setSeconds] = React.useState(0)
|
|
9
|
+
React.useEffect(() => {
|
|
10
|
+
const timeout = setTimeout(() => setSeconds(seconds + 1), 1000)
|
|
11
|
+
return () => clearTimeout(timeout)
|
|
12
|
+
}, [seconds])
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<TextBlock align="flex-start">
|
|
17
|
+
I am a dialog. I have been open for: {seconds} second{seconds === 1 ? "" : "s"}
|
|
18
|
+
</TextBlock>
|
|
19
|
+
{/* The hook in Advice will suspend until the advice is loaded. */}
|
|
20
|
+
<React.Suspense fallback={<Loading />}>
|
|
21
|
+
<Advice recordId={recordId} />
|
|
22
|
+
</React.Suspense>
|
|
23
|
+
</>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -7,15 +7,17 @@
|
|
|
7
7
|
"attio": "attio",
|
|
8
8
|
"dev": "attio dev",
|
|
9
9
|
"build": "attio build",
|
|
10
|
+
"format": "yarn run -T prettier --ignore-path $(yarn run -T find-up .prettierignore) --check ./",
|
|
11
|
+
"format-fix": "yarn run format --write ./",
|
|
10
12
|
"clean": "rm -rf lib && rm -rf dist",
|
|
11
|
-
"lint": "eslint \"src/**/*.{ts,tsx,js,tsx}\""
|
|
13
|
+
"lint": "ATTIO_LINT_MODE='exhaustive' eslint \"src/**/*.{ts,tsx,js,tsx}\""
|
|
12
14
|
},
|
|
13
15
|
"devDependencies": {
|
|
14
|
-
"@types/react": "18.3.
|
|
15
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
16
|
-
"@typescript-eslint/parser": "^
|
|
17
|
-
"eslint": "^8.57.
|
|
18
|
-
"eslint-plugin-react": "^7.
|
|
16
|
+
"@types/react": "~18.3.12",
|
|
17
|
+
"@typescript-eslint/eslint-plugin": "^8.16.0",
|
|
18
|
+
"@typescript-eslint/parser": "^8.16.0",
|
|
19
|
+
"eslint": "^8.57.1",
|
|
20
|
+
"eslint-plugin-react": "^7.37.2",
|
|
19
21
|
"eslint-plugin-react-hooks": "^4.6.2",
|
|
20
22
|
"typescript": "5.4.5"
|
|
21
23
|
},
|