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.
Files changed (47) hide show
  1. package/lib/attio.js +1 -1
  2. package/lib/build/client/generate-client-entry.js +34 -22
  3. package/lib/build.js +1 -0
  4. package/lib/client/bulk-record-action.d.ts +54 -0
  5. package/lib/client/components/index.d.ts +0 -2
  6. package/lib/client/hooks/index.d.ts +1 -1
  7. package/lib/client/hooks/use-workspace.d.ts +13 -0
  8. package/lib/client/index.d.ts +3 -2
  9. package/lib/client/record-action.d.ts +53 -0
  10. package/lib/client/show-dialog.d.ts +33 -0
  11. package/lib/client/show-toast.d.ts +1 -1
  12. package/lib/commands/dev.js +2 -0
  13. package/lib/components/BuildError.js +7 -1
  14. package/lib/components/CodeGenErrors.js +2 -2
  15. package/lib/components/Log.js +69 -0
  16. package/lib/machines/dev-machine.js +6 -3
  17. package/lib/machines/js-machine.js +0 -8
  18. package/lib/server/attio-fetch.d.ts +57 -0
  19. package/lib/server/index.d.ts +1 -0
  20. package/lib/templates/common/src/assets/icon.png +0 -0
  21. package/lib/templates/javascript/package.json +5 -3
  22. package/lib/templates/javascript/src/advice.jsx +16 -0
  23. package/lib/templates/javascript/src/get-advice.server.js +6 -0
  24. package/lib/templates/javascript/src/hello-world-action.jsx +17 -0
  25. package/lib/templates/javascript/src/hello-world-dialog.jsx +25 -0
  26. package/lib/templates/typescript/package.json +8 -6
  27. package/lib/templates/typescript/src/advice.tsx +16 -0
  28. package/lib/templates/typescript/src/get-advice.server.ts +6 -0
  29. package/lib/templates/typescript/src/hello-world-action.tsx +17 -0
  30. package/lib/templates/typescript/src/hello-world-dialog.tsx +25 -0
  31. package/lib/tsconfig.tsbuildinfo +1 -1
  32. package/lib/util/copy-with-replace.js +8 -3
  33. package/lib/util/find-surface-exports/find-surface-exports.js +45 -0
  34. package/lib/util/find-surface-exports/generate-random-file-name.js +13 -0
  35. package/lib/util/find-surface-exports/parse-file-exports.js +98 -0
  36. package/lib/util/find-surface-exports/surface-types.js +24 -0
  37. package/lib/util/find-surface-exports/walk-dir.js +25 -0
  38. package/lib/util/surfaces.js +4 -0
  39. package/package.json +3 -3
  40. package/schema.graphql +14 -0
  41. package/lib/client/components/action.d.ts +0 -31
  42. package/lib/client/components/multistep.d.ts +0 -24
  43. package/lib/client/hooks/use-dialog.d.ts +0 -71
  44. package/lib/client/register-bulk-record-action.d.ts +0 -36
  45. package/lib/client/register-record-action.d.ts +0 -36
  46. package/lib/templates/javascript/src/app.jsx +0 -6
  47. package/lib/templates/typescript/src/app.tsx +0 -6
package/lib/attio.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node --no-warnings=ExperimentalWarning
1
+ #!/usr/bin/env node
2
2
  import Pastel from "pastel";
3
3
  const app = new Pastel({
4
4
  name: "attio",
@@ -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, onAppFileFound, onAppFileMissing, }) {
5
- let appFilePathAbsolute = null;
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
- return `
35
- ${appFilePathAbsolute ? `import ${JSON.stringify(appFilePathAbsolute)};` : ""}
36
-
37
- ${assets.map((asset, index) => `import A${index} from ${JSON.stringify(asset.path)};`).join("\n")}
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
@@ -7,6 +7,7 @@ const buildErrorSchema = z.object({
7
7
  length: z.number(),
8
8
  line: z.number(),
9
9
  lineText: z.string(),
10
+ additionalLines: z.array(z.string()).optional(),
10
11
  namespace: z.string(),
11
12
  suggestion: z.string(),
12
13
  }),
@@ -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";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Get information about the Attio workspace.
3
+ */
4
+ export declare function useWorkspace(): {
5
+ /**
6
+ * The name of the workspace.
7
+ */
8
+ workspaceName: string;
9
+ /**
10
+ * The slug of the workspace
11
+ */
12
+ workspaceSlug: string;
13
+ };
@@ -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 {};
@@ -11,7 +11,7 @@ export interface ToastOptions {
11
11
  /**
12
12
  * The text that will be displayed below the title.
13
13
  */
14
- text: string;
14
+ text?: string;
15
15
  /**
16
16
  * An optional action to be displayed at the bottom of the toast.
17
17
  */
@@ -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 {};
@@ -1,3 +1,4 @@
1
1
  export * from "./env";
2
2
  export * from "./connections";
3
3
  export * from "./webhook-handlers";
4
+ export * from "./attio-fetch";
@@ -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.0",
15
- "eslint-plugin-react": "^7.34.2",
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.3",
15
- "@typescript-eslint/eslint-plugin": "^5.62.0",
16
- "@typescript-eslint/parser": "^5.62.0",
17
- "eslint": "^8.57.0",
18
- "eslint-plugin-react": "^7.34.2",
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
  },