@conexus-x/next-view 1.0.2 → 1.0.3

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/.env CHANGED
@@ -1,21 +1,31 @@
1
1
  # ---------------------------------------------------------------------------
2
- # DEVELOPER MODE. None of this is used by your view in production.
2
+ # DEVELOPER MODE ONLY — none of this ships with your extension.
3
3
  #
4
- # The values here power the dev preview at /dev, which frames your view the way
5
- # a Conexus X board does and feeds it REAL data from your workspace.
4
+ # A deployed view holds no key and picks no workspace. It is framed by a board,
5
+ # and the board tells it which workspace it is in and who is looking at it. The
6
+ # values here exist so you can build against real data before that happens.
6
7
  # ---------------------------------------------------------------------------
7
8
 
8
- # Your Conexus X API key. Find it in the app under Settings > API.
9
+ # Your Conexus X API key find it in the app under Developer > API Key.
9
10
  #
10
- # It is read by the dev server ONLY and never reaches the browser: requests go
11
- # to /api/conexus/* on this app, which attaches the key server-side and forwards
12
- # them. A permanent credential in client-side JavaScript is a permanent
13
- # credential in every browser devtools panel that ever opens your view.
11
+ # Read by the dev server ONLY; it never reaches the browser. A permanent
12
+ # credential in client-side JavaScript is one devtools panel away from being
13
+ # somebody else's.
14
14
  API_KEY=Conexus_x_api_key
15
15
 
16
+ # The workspace this extension is built for.
17
+ #
18
+ # An extension is written against a SHAPE — particular boards, particular
19
+ # columns — and every workspace holds different ones, so a view built for one
20
+ # is meaningless pointed at another. Naming it here makes the dev page show
21
+ # that workspace's boards and nothing else.
22
+ #
23
+ # Leave it blank the first time: the dev page then lists your workspaces with
24
+ # their ids so you can copy one in.
25
+ WORKSPACE_ID=
26
+
16
27
  # Where the Conexus X API lives.
17
28
  CONEXUS_API_URL=http://localhost:4040/api
18
29
 
19
30
  # Which origins may frame this view, space or comma separated.
20
- # Add your tunnel's CRM origin here when previewing inside the real app.
21
31
  CONEXUS_ORIGINS=http://localhost:3000 https://conexus-x.vercel.app
package/README.md CHANGED
@@ -28,33 +28,43 @@ Nothing in this repo records the link, which is the point.
28
28
 
29
29
  ## Developer mode
30
30
 
31
- You do not need a Conexus X board to start. Put your API key in `.env` and open
32
- the developer page it lists the workspaces that key can see, which is the
33
- shortest proof that your key works and the API is reachable.
31
+ An extension is built for **one workspace**. Every workspace holds different
32
+ boards and different columns, so a view written for Sales is meaningless
33
+ pointed at Marketing you name the one you mean, once, in `.env`.
34
34
 
35
35
  ```bash
36
- # .env — already in your project, just fill it in
37
- API_KEY=Conexus_x_api_key # <- replace with your key (Settings > API)
38
- CONEXUS_API_URL=http://localhost:4040/api
36
+ # .env — the only env file in this project. Fill in two values.
37
+ API_KEY=Conexus_x_api_key # <- your key: Conexus X > Developer > API Key
38
+ WORKSPACE_ID= # <- leave blank the first time
39
39
  ```
40
40
 
41
41
  ```bash
42
42
  npm run dev
43
43
  ```
44
44
 
45
- Then open **http://localhost:5173/dev**.
45
+ Open **http://localhost:5173/dev**.
46
46
 
47
- **The key never reaches the browser.** The page is a React Server Component: `API_KEY` is read in Node and only the finished list is sent to the browser.
47
+ With `WORKSPACE_ID` blank it lists your workspaces **with their ids**. Copy the
48
+ one you are building for into `.env`, reload, and the page then shows that
49
+ workspace and its boards — and only those.
50
+
51
+ **The pin is development only, and it has to be.** In production the board
52
+ decides which workspace your view is mounted in and hands it over as context. A
53
+ view that chose its own workspace would be a view that could read one it was
54
+ never mounted in. This page mirrors what the host will give you; it does not
55
+ override it.
56
+
57
+ **The key never reaches the browser.** The dev page is a React Server Component: `API_KEY` is read in Node and only the finished list is sent down.
48
58
  A permanent credential in client-side JavaScript is one devtools panel away
49
59
  from being somebody else's.
50
60
 
51
- `.env` is gitignored, so your real key cannot be committed by accident. The
52
- committed copy is `.env.example`.
61
+ `.env` is gitignored, so your real key cannot be committed by accident — and it
62
+ still ships inside the npm package, so a scaffolded project always has one
63
+ ready to fill in.
53
64
 
54
- The developer page is not part of a production build, and **your view never
55
- uses the API key** a deployed view receives data through the SDK, with the
56
- permissions of whoever is looking at the board. The key is scaffolding for
57
- development only.
65
+ The dev page is not part of a production build, and **your view never uses the
66
+ API key**: a deployed view receives data through the SDK, with the permissions
67
+ of whoever is looking at the board.
58
68
 
59
69
  ## Previewing inside the real CRM
60
70
 
package/app/dev/page.tsx CHANGED
@@ -3,21 +3,25 @@ import { notFound } from "next/navigation";
3
3
  /**
4
4
  * DEVELOPER MODE — http://localhost:5173/dev
5
5
  *
6
- * Put your API key in .env, reload, and this lists the workspaces that key can
7
- * see. It is the shortest possible proof that your key works and the API is
8
- * reachable, which is the thing that is actually wrong the first time nothing
9
- * renders.
6
+ * Put your API key and the workspace you are building for in .env, reload, and
7
+ * this shows that workspace's boards. It is the shortest proof that your key
8
+ * works, the API is reachable, and you are pointed at the right data — which is
9
+ * what is actually wrong the first time nothing renders.
10
10
  *
11
- * A SERVER COMPONENT, deliberately. The key is read here, in Node, and only the
12
- * finished list is sent to the browser so the key never appears in a bundle,
13
- * a network tab, or anybody's devtools. Fetching this from the browser would
14
- * put a permanent credential in client-side JavaScript, and a permanent
15
- * credential in client-side JavaScript is one screenshot away from being
16
- * somebody else's.
11
+ * WHY A WORKSPACE IS PINNED IN .env: an extension is written against a SHAPE
12
+ * particular boards, particular columnsand every workspace holds different
13
+ * ones. A view built for Sales is meaningless pointed at Marketing, so the
14
+ * developer names the one they mean instead of browsing all of them.
15
+ *
16
+ * THAT PIN IS DEVELOPMENT ONLY, and it has to be. In production the BOARD
17
+ * decides which workspace a view is mounted in and hands it over as context; a
18
+ * view that chose its own would be a view that could read a workspace it was
19
+ * never mounted in. This file mirrors what the host will hand you. It does not
20
+ * override it.
17
21
  *
18
- * Your VIEW does not work this way and must not: it runs framed by a board and
19
- * receives data through the SDK, with the signed-in person's own permissions.
20
- * This page is scaffolding for development, nothing more.
22
+ * A SERVER COMPONENT, deliberately. The key is read here, in Node, and only the
23
+ * finished list is sent to the browser so it never appears in a bundle, a
24
+ * network tab, or anybody's devtools.
21
25
  */
22
26
 
23
27
  // Never prerendered: it reads .env and the network at request time.
@@ -25,74 +29,112 @@ export const dynamic = "force-dynamic";
25
29
 
26
30
  const PLACEHOLDER = "Conexus_x_api_key";
27
31
 
32
+ interface Workspace {
33
+ _id: string;
34
+ name?: string;
35
+ totalModules?: number;
36
+ }
37
+
28
38
  interface WorkspaceMembership {
29
39
  _id: string;
30
40
  role?: string;
31
- status?: string;
32
- workspace?: {
33
- _id: string;
34
- name?: string;
35
- description?: string;
36
- totalModules?: number;
37
- } | null;
41
+ workspace?: Workspace | null;
42
+ }
43
+
44
+ interface Board {
45
+ _id: string;
46
+ name?: string;
47
+ description?: string;
48
+ visibility?: string;
49
+ totalRecords?: number;
38
50
  }
39
51
 
40
- type LoadResult =
41
- | { ok: true; workspaces: WorkspaceMembership[] }
42
- | { ok: false; title: string; detail: string };
52
+ type DevState =
53
+ | { kind: "error"; title: string; detail: string }
54
+ /** No WORKSPACE_ID yet show what is available so one can be copied in. */
55
+ | { kind: "pick"; workspaces: WorkspaceMembership[] }
56
+ | { kind: "pinned"; workspace: Workspace; role: string | undefined; boards: Board[] };
57
+
58
+ const apiUrl = () =>
59
+ (process.env.CONEXUS_API_URL ?? "http://localhost:4040/api").replace(/\/$/, "");
43
60
 
44
- const loadWorkspaces = async (): Promise<LoadResult> => {
61
+ const load = async (): Promise<DevState> => {
45
62
  const apiKey = process.env.API_KEY;
46
- const apiUrl = (process.env.CONEXUS_API_URL ?? "http://localhost:4040/api").replace(/\/$/, "");
63
+ const workspaceId = (process.env.WORKSPACE_ID ?? "").trim();
47
64
 
48
65
  if (!apiKey) {
49
66
  return {
50
- ok: false,
67
+ kind: "error",
51
68
  title: "No API key",
52
- detail: "Add API_KEY to .env in this project, then reload."
69
+ detail: "Add API_KEY to .env in this project, then reload.",
53
70
  };
54
71
  }
55
72
 
56
73
  if (apiKey === PLACEHOLDER) {
57
74
  return {
58
- ok: false,
75
+ kind: "error",
59
76
  title: "The API key is still the placeholder",
60
- detail: `Replace API_KEY=${PLACEHOLDER} in .env with your own key from Conexus X (Settings › API), then reload.`
77
+ detail: `Replace API_KEY=${PLACEHOLDER} in .env with your own key from Conexus X (Developer › API Key), then reload.`,
61
78
  };
62
79
  }
63
80
 
64
- try {
65
- const response = await fetch(`${apiUrl}/workspaces`, {
81
+ const call = async (path: string) => {
82
+ const response = await fetch(`${apiUrl()}${path}`, {
66
83
  headers: { "x-api-key": apiKey },
67
- cache: "no-store"
84
+ cache: "no-store",
68
85
  });
69
86
 
70
87
  if (response.status === 401) {
71
- return {
72
- ok: false,
73
- title: "That key was rejected",
74
- detail: "The API answered 401. Check the key, and that the account it belongs to is still active."
75
- };
88
+ throw new Error(
89
+ "The API answered 401. Check the key, and that the account it belongs to is still active."
90
+ );
76
91
  }
77
92
 
78
93
  if (!response.ok) {
94
+ throw new Error(`The API answered ${response.status}: ${(await response.text()).slice(0, 200)}`);
95
+ }
96
+
97
+ return response.json() as Promise<Record<string, unknown>>;
98
+ };
99
+
100
+ try {
101
+ const body = await call("/workspaces");
102
+ const memberships = (body["workspaces"] as WorkspaceMembership[] | undefined) ?? [];
103
+
104
+ if (!workspaceId) {
105
+ return { kind: "pick", workspaces: memberships };
106
+ }
107
+
108
+ const mine = memberships.find((row) => row.workspace?._id === workspaceId);
109
+
110
+ // A pinned id that is not one of yours is the single most confusing
111
+ // failure here — every call succeeds and the page is simply empty — so
112
+ // it is named rather than left to look like "no boards".
113
+ if (!mine?.workspace) {
79
114
  return {
80
- ok: false,
81
- title: `The API answered ${response.status}`,
82
- detail: (await response.text()).slice(0, 300)
115
+ kind: "error",
116
+ title: "WORKSPACE_ID does not match a workspace you are in",
117
+ detail:
118
+ `.env points at ${workspaceId}, which is not among the ${memberships.length} ` +
119
+ "workspace(s) this key can see. Clear WORKSPACE_ID to list them again.",
83
120
  };
84
121
  }
85
122
 
86
- const body = (await response.json()) as { workspaces?: WorkspaceMembership[] };
123
+ const boardsBody = await call(`/modules/${workspaceId}`);
87
124
 
88
- return { ok: true, workspaces: body.workspaces ?? [] };
125
+ return {
126
+ kind: "pinned",
127
+ workspace: mine.workspace,
128
+ role: mine.role,
129
+ boards: (boardsBody["modules"] as Board[] | undefined) ?? [],
130
+ };
89
131
  } catch (error) {
90
132
  return {
91
- ok: false,
133
+ kind: "error",
92
134
  title: "Could not reach the API",
93
135
  detail:
94
- `Tried ${apiUrl}/workspaces — ${error instanceof Error ? error.message : String(error)}. ` +
95
- "Is the Conexus X API running, and is CONEXUS_API_URL right?"
136
+ `${error instanceof Error ? error.message : String(error)}\n\n` +
137
+ `Tried ${apiUrl()}. Is the Conexus X API running, and is CONEXUS_API_URL right?`,
96
138
  };
97
139
  }
98
140
  };
@@ -102,60 +144,129 @@ export default async function DevPage() {
102
144
  // business in a deployed view, so it is not there.
103
145
  if (process.env.NODE_ENV === "production") notFound();
104
146
 
105
- const result = await loadWorkspaces();
147
+ const state = await load();
106
148
 
107
149
  return (
108
150
  <main className="mx-auto max-w-2xl p-6">
109
151
  <header className="mb-5">
110
152
  <h1 className="text-lg font-semibold">Developer mode</h1>
111
153
  <p className="mt-1 text-[13px] text-muted">
112
- Workspaces visible to the API key in <code>.env</code>.
154
+ {state.kind === "pinned"
155
+ ? "The workspace this extension is built for, and its boards."
156
+ : "Reading the API key in .env."}
113
157
  </p>
114
158
  </header>
115
159
 
116
- {!result.ok ? (
160
+ {state.kind === "error" ? (
117
161
  <div className="rounded-lg border border-danger bg-danger-soft px-4 py-3 text-danger">
118
- <strong className="font-semibold">{result.title}</strong>
119
- <p className="mt-1.5 text-[13px] whitespace-pre-wrap">{result.detail}</p>
162
+ <strong className="font-semibold">{state.title}</strong>
163
+ <p className="mt-1.5 text-[13px] whitespace-pre-wrap">{state.detail}</p>
120
164
  </div>
121
- ) : result.workspaces.length === 0 ? (
122
- <p className="text-[13px] text-muted">
123
- The key works this account is not a member of any workspace yet.
124
- </p>
165
+ ) : state.kind === "pick" ? (
166
+ <>
167
+ <div className="mb-4 rounded-lg border border-line bg-accent-soft px-4 py-3">
168
+ <strong className="font-semibold">Pick the workspace you are building for</strong>
169
+ <p className="mt-1 text-[13px]">
170
+ Copy its id into <code>WORKSPACE_ID</code> in <code>.env</code> and
171
+ reload. This view will then show that workspace&apos;s boards — and
172
+ only those.
173
+ </p>
174
+ </div>
175
+
176
+ {state.workspaces.length === 0 ? (
177
+ <p className="text-[13px] text-muted">
178
+ The key works — this account is not a member of any workspace yet.
179
+ </p>
180
+ ) : (
181
+ <ul className="rounded-lg border border-line">
182
+ {state.workspaces.map((membership) => (
183
+ <li
184
+ key={membership._id}
185
+ className="flex items-center gap-3 border-b border-line px-4 py-3 last:border-b-0"
186
+ >
187
+ <div className="min-w-0 flex-1">
188
+ <div className="truncate font-medium">
189
+ {membership.workspace?.name ?? "Untitled workspace"}
190
+ </div>
191
+ <div className="mt-0.5 font-mono text-[12px] text-muted">
192
+ {membership.workspace?._id}
193
+ </div>
194
+ </div>
195
+
196
+ {membership.role ? (
197
+ <span className="shrink-0 rounded-md border border-line px-2 py-0.5 text-[12px] text-muted">
198
+ {membership.role}
199
+ </span>
200
+ ) : null}
201
+
202
+ <span className="shrink-0 text-[12px] text-muted">
203
+ {membership.workspace?.totalModules ?? 0} boards
204
+ </span>
205
+ </li>
206
+ ))}
207
+ </ul>
208
+ )}
209
+ </>
125
210
  ) : (
126
- <ul className="rounded-lg border border-line">
127
- {result.workspaces.map((membership) => (
128
- <li
129
- key={membership._id}
130
- className="flex items-center gap-3 border-b border-line px-4 py-3 last:border-b-0"
131
- >
132
- <div className="min-w-0 flex-1">
133
- <div className="truncate font-medium">
134
- {membership.workspace?.name ?? "Untitled workspace"}
135
- </div>
136
- <div className="mt-0.5 font-mono text-[12px] text-muted">
137
- {membership.workspace?._id}
138
- </div>
211
+ <>
212
+ <div className="mb-4 flex items-center gap-3 rounded-lg border border-line px-4 py-3">
213
+ <div className="min-w-0 flex-1">
214
+ <div className="truncate text-[15px] font-semibold">
215
+ {state.workspace.name ?? "Untitled workspace"}
139
216
  </div>
217
+ <div className="mt-0.5 font-mono text-[12px] text-muted">
218
+ {state.workspace._id}
219
+ </div>
220
+ </div>
140
221
 
141
- {membership.role ? (
142
- <span className="shrink-0 rounded-md border border-line px-2 py-0.5 text-[12px] text-muted">
143
- {membership.role}
144
- </span>
145
- ) : null}
146
-
147
- <span className="shrink-0 text-[12px] text-muted">
148
- {membership.workspace?.totalModules ?? 0} boards
222
+ {state.role ? (
223
+ <span className="shrink-0 rounded-md border border-line px-2 py-0.5 text-[12px] text-muted">
224
+ {state.role}
149
225
  </span>
150
- </li>
151
- ))}
152
- </ul>
226
+ ) : null}
227
+ </div>
228
+
229
+ {state.boards.length === 0 ? (
230
+ <p className="text-[13px] text-muted">
231
+ This workspace has no boards yet. Create one in Conexus X and
232
+ reload.
233
+ </p>
234
+ ) : (
235
+ <ul className="rounded-lg border border-line">
236
+ {state.boards.map((board) => (
237
+ <li
238
+ key={board._id}
239
+ className="flex items-center gap-3 border-b border-line px-4 py-3 last:border-b-0"
240
+ >
241
+ <div className="min-w-0 flex-1">
242
+ <div className="truncate font-medium">
243
+ {board.name ?? "Untitled board"}
244
+ </div>
245
+ <div className="mt-0.5 font-mono text-[12px] text-muted">
246
+ {board._id}
247
+ </div>
248
+ </div>
249
+
250
+ {board.visibility ? (
251
+ <span className="shrink-0 rounded-md border border-line px-2 py-0.5 text-[12px] text-muted">
252
+ {board.visibility}
253
+ </span>
254
+ ) : null}
255
+
256
+ <span className="shrink-0 text-[12px] text-muted">
257
+ {board.totalRecords ?? 0} records
258
+ </span>
259
+ </li>
260
+ ))}
261
+ </ul>
262
+ )}
263
+ </>
153
264
  )}
154
265
 
155
266
  <p className="mt-5 text-[12px] text-muted">
156
- This page is development-only and returns 404 in a production build.
157
- Your view itself never uses the API key — it receives data through
158
- the SDK, with the permissions of whoever is looking at the board.
267
+ Development-only, and returns 404 in a production build. Your view itself
268
+ never uses the API key and never chooses a workspace the board it is
269
+ mounted in tells it which one, with the permissions of whoever is looking.
159
270
  </p>
160
271
  </main>
161
272
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conexus-x/next-view",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "private": false,
5
5
  "description": "Custom view for Conexus X — Next.js starter, wired to @conexus-x/sdk.",
6
6
  "scripts": {
@@ -35,7 +35,6 @@
35
35
  "tsconfig.json",
36
36
  "conexus.manifest.json",
37
37
  ".env",
38
- ".env.example",
39
38
  "README.md",
40
39
  ".gitignore"
41
40
  ]
package/.env.example DELETED
@@ -1,21 +0,0 @@
1
- # ---------------------------------------------------------------------------
2
- # DEVELOPER MODE. None of this is used by your view in production.
3
- #
4
- # The values here power the dev preview at /dev, which frames your view the way
5
- # a Conexus X board does and feeds it REAL data from your workspace.
6
- # ---------------------------------------------------------------------------
7
-
8
- # Your Conexus X API key. Find it in the app under Settings > API.
9
- #
10
- # It is read by the dev server ONLY and never reaches the browser: requests go
11
- # to /api/conexus/* on this app, which attaches the key server-side and forwards
12
- # them. A permanent credential in client-side JavaScript is a permanent
13
- # credential in every browser devtools panel that ever opens your view.
14
- API_KEY=Conexus_x_api_key
15
-
16
- # Where the Conexus X API lives.
17
- CONEXUS_API_URL=http://localhost:4040/api
18
-
19
- # Which origins may frame this view, space or comma separated.
20
- # Add your tunnel's CRM origin here when previewing inside the real app.
21
- CONEXUS_ORIGINS=http://localhost:3000 https://conexus-x.vercel.app