attio 0.0.1-experimental.20241031 → 0.0.1-experimental.20241203.2

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 (43) hide show
  1. package/lib/api/handle-error.js +1 -1
  2. package/lib/attio.js +1 -1
  3. package/lib/build/client/generate-client-entry.js +34 -22
  4. package/lib/client/alert.d.ts +59 -0
  5. package/lib/client/bulk-record-action.d.ts +54 -0
  6. package/lib/client/components/index.d.ts +0 -2
  7. package/lib/client/confirm.d.ts +80 -0
  8. package/lib/client/hooks/index.d.ts +1 -1
  9. package/lib/client/hooks/use-workspace.d.ts +13 -0
  10. package/lib/client/index.d.ts +5 -2
  11. package/lib/client/record-action.d.ts +53 -0
  12. package/lib/client/show-dialog.d.ts +33 -0
  13. package/lib/client/show-toast.d.ts +1 -1
  14. package/lib/commands/dev.js +2 -0
  15. package/lib/components/CodeGenErrors.js +2 -2
  16. package/lib/components/Log.js +69 -0
  17. package/lib/machines/dev-machine.js +6 -3
  18. package/lib/machines/js-machine.js +0 -4
  19. package/lib/templates/common/src/assets/icon.png +0 -0
  20. package/lib/templates/javascript/package.json +6 -4
  21. package/lib/templates/javascript/src/advice.jsx +16 -0
  22. package/lib/templates/javascript/src/get-advice.server.js +6 -0
  23. package/lib/templates/javascript/src/hello-world-action.jsx +17 -0
  24. package/lib/templates/javascript/src/hello-world-dialog.jsx +25 -0
  25. package/lib/templates/typescript/package.json +8 -6
  26. package/lib/templates/typescript/src/advice.tsx +16 -0
  27. package/lib/templates/typescript/src/get-advice.server.ts +6 -0
  28. package/lib/templates/typescript/src/hello-world-action.tsx +17 -0
  29. package/lib/templates/typescript/src/hello-world-dialog.tsx +25 -0
  30. package/lib/tsconfig.tsbuildinfo +1 -1
  31. package/lib/util/copy-with-replace.js +10 -5
  32. package/lib/util/create-directory.js +1 -1
  33. package/lib/util/find-node-modules-path.js +1 -1
  34. package/lib/util/find-surface-exports.js +76 -0
  35. package/lib/util/load-env.js +1 -1
  36. package/lib/util/surfaces.js +4 -0
  37. package/package.json +4 -4
  38. package/schema.graphql +14 -0
  39. package/lib/client/components/action.d.ts +0 -31
  40. package/lib/client/components/multistep.d.ts +0 -24
  41. package/lib/client/hooks/use-dialog.d.ts +0 -71
  42. package/lib/client/register-bulk-record-action.d.ts +0 -36
  43. package/lib/client/register-record-action.d.ts +0 -36
@@ -10,7 +10,7 @@ export async function handleError(response) {
10
10
  try {
11
11
  json = JSON.parse(text);
12
12
  }
13
- catch (error) {
13
+ catch {
14
14
  throw new Error(`Error parsing JSON: ${JSON.stringify(text)}`);
15
15
  }
16
16
  const error = serverErrorSchema.parse(json);
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.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
  }
@@ -0,0 +1,59 @@
1
+ interface BaseAlertOptions {
2
+ /**
3
+ * Label for the "OK" button.
4
+ *
5
+ * Defaults to "OK".
6
+ * @default "OK"
7
+ */
8
+ okLabel?: string;
9
+ }
10
+ interface AlertWithNoTextOptions extends BaseAlertOptions {
11
+ /**
12
+ * The title of the prompt.
13
+ */
14
+ title: string;
15
+ /**
16
+ * No text will be displayed.
17
+ */
18
+ text?: never;
19
+ }
20
+ interface AlertNoTitleOptions extends BaseAlertOptions {
21
+ /**
22
+ * No title will be displayed.
23
+ */
24
+ title?: never;
25
+ /**
26
+ * Text to display.
27
+ */
28
+ text: string;
29
+ }
30
+ interface AlertWithTextAndTitleOptions extends BaseAlertOptions {
31
+ /**
32
+ * The title of the prompt.
33
+ */
34
+ title: string;
35
+ /**
36
+ * The text that will be displayed below the title.
37
+ */
38
+ text: string;
39
+ }
40
+ export type AlertOptions = AlertWithNoTextOptions | AlertNoTitleOptions | AlertWithTextAndTitleOptions;
41
+ /**
42
+ * Shows an alert to the user.
43
+ *
44
+ * ## EXAMPLE USAGE
45
+ *
46
+ * ----
47
+ * ```tsx
48
+ * // app.tsx
49
+ * import { alert } from "attio/client";
50
+ *
51
+ * await alert({
52
+ * title: "Insufficient privileges",
53
+ * text: "You do not have permission to perform this action.",
54
+ * });
55
+ * ```
56
+ * ----
57
+ */
58
+ export declare function alert(options: AlertOptions): Promise<void>;
59
+ export {};
@@ -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";
@@ -0,0 +1,80 @@
1
+ import { Icon } from "./icon";
2
+ interface BaseConfirmOptions {
3
+ /**
4
+ * Label for the "Cancel" button.
5
+ *
6
+ * Defaults to "Cancel".
7
+ * @default "Cancel"
8
+ */
9
+ cancelLabel?: string;
10
+ /**
11
+ * Label for the "Confirm" button.
12
+ *
13
+ * Defaults to "OK".
14
+ * @default "OK"
15
+ */
16
+ confirmLabel?: string;
17
+ /**
18
+ * Icon to display on the confirm button.
19
+ */
20
+ confirmIcon?: Icon;
21
+ /**
22
+ * Variant of the "confirm" button.
23
+ *
24
+ * Defaults to "primary".
25
+ * @default "primary"
26
+ */
27
+ confirmVariant?: "primary" | "secondary" | "secondary-destructive" | "destructive";
28
+ }
29
+ interface ConfirmNoTextOptions extends BaseConfirmOptions {
30
+ /**
31
+ * Title of the prompt.
32
+ */
33
+ title: string;
34
+ /**
35
+ * No text will be displayed.
36
+ */
37
+ text?: never;
38
+ }
39
+ interface ConfirmNoTitleOptions extends BaseConfirmOptions {
40
+ /**
41
+ * No title will be displayed.
42
+ */
43
+ title?: never;
44
+ /**
45
+ * Text to display
46
+ */
47
+ text: string;
48
+ }
49
+ interface ConfirmWithTextAndTitleOptions extends BaseConfirmOptions {
50
+ /**
51
+ * Title of the prompt.
52
+ */
53
+ title: string;
54
+ /**
55
+ * Text to display below the title.
56
+ */
57
+ text: string;
58
+ }
59
+ export type ConfirmOptions = ConfirmNoTextOptions | ConfirmNoTitleOptions | ConfirmWithTextAndTitleOptions;
60
+ /**
61
+ * Shows a confirm prompt to the user.
62
+ *
63
+ * ## EXAMPLE USAGE
64
+ *
65
+ * ----
66
+ * ```tsx
67
+ * // app.tsx
68
+ * import { showPrompt } from "attio/client";
69
+ *
70
+ * await confirm({
71
+ * title: "Are you sure you want to delete this item?",
72
+ * text: "This action cannot be undone.",
73
+ * cancelLabel: "Cancel",
74
+ * confirmLabel: "Delete",
75
+ * });
76
+ * ```
77
+ * ----
78
+ */
79
+ export declare function confirm(options: ConfirmOptions): Promise<boolean>;
80
+ export {};
@@ -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,7 +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";
13
+ export { confirm, ConfirmOptions } from "./confirm.js";
14
+ export { alert, AlertOptions } from "./alert.js";
15
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 ",
@@ -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))
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,
@@ -35,10 +35,6 @@ export const jsMachine = setup({
35
35
  const js = await generateClientEntry({
36
36
  srcDirAbsolute: path.resolve(srcDir),
37
37
  assetsDirAbsolute: path.resolve(assetsDir),
38
- onAppFileMissing: () => {
39
- },
40
- onAppFileFound: () => {
41
- },
42
38
  });
43
39
  if (js === lastJS) {
44
40
  return;
@@ -7,13 +7,15 @@
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-plugin-react-hooks": "^4.6.2"
16
+ "eslint": "^8.57.1",
17
+ "eslint-plugin-react": "^7.37.2",
18
+ "eslint-plugin-react-hooks": "^4.5.0"
17
19
  },
18
20
  "dependencies": {
19
21
  "@eslint/eslintrc": "^3.1.0",
@@ -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
+ }