@conexus-x/next-view 1.0.1 → 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 +31 -0
- package/.gitignore +1 -0
- package/README.md +65 -0
- package/app/dev/page.tsx +273 -0
- package/next.config.ts +16 -0
- package/package.json +4 -2
package/.env
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ---------------------------------------------------------------------------
|
|
2
|
+
# DEVELOPER MODE ONLY — none of this ships with your extension.
|
|
3
|
+
#
|
|
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.
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
# Your Conexus X API key — find it in the app under Developer > API Key.
|
|
10
|
+
#
|
|
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
|
+
API_KEY=Conexus_x_api_key
|
|
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
|
+
|
|
27
|
+
# Where the Conexus X API lives.
|
|
28
|
+
CONEXUS_API_URL=http://localhost:4040/api
|
|
29
|
+
|
|
30
|
+
# Which origins may frame this view, space or comma separated.
|
|
31
|
+
CONEXUS_ORIGINS=http://localhost:3000 https://conexus-x.vercel.app
|
package/.gitignore
CHANGED
package/README.md
CHANGED
|
@@ -26,6 +26,71 @@ cd - && npm link @conexus-x/sdk
|
|
|
26
26
|
Nothing in this repo records the link, which is the point.
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
## Developer mode
|
|
30
|
+
|
|
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
|
+
|
|
35
|
+
```bash
|
|
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
|
+
```
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm run dev
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Open **http://localhost:5173/dev**.
|
|
46
|
+
|
|
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.
|
|
58
|
+
A permanent credential in client-side JavaScript is one devtools panel away
|
|
59
|
+
from being somebody else's.
|
|
60
|
+
|
|
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.
|
|
64
|
+
|
|
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.
|
|
68
|
+
|
|
69
|
+
## Previewing inside the real CRM
|
|
70
|
+
|
|
71
|
+
To see your view running inside an actual Conexus X board while you develop,
|
|
72
|
+
give it a public URL:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run dev # leave this running
|
|
76
|
+
npm run tunnel # in a second terminal
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
That prints a `https://….trycloudflare.com` address. Then:
|
|
80
|
+
|
|
81
|
+
1. Put it in `entry` in `conexus.manifest.json`.
|
|
82
|
+
2. Add the CRM's origin to `CONEXUS_ORIGINS` in `.env` and restart the dev
|
|
83
|
+
server, so the view allows that origin to frame it.
|
|
84
|
+
|
|
85
|
+
The tunnel is Cloudflare's free quick tunnel — no account, no signup. It is
|
|
86
|
+
fetched on first use rather than installed with the project, because the binary
|
|
87
|
+
is large and most runs never need it. Quick tunnels have no uptime guarantee and
|
|
88
|
+
the URL changes every run; they are for previewing, not for shipping.
|
|
89
|
+
|
|
90
|
+
Chosen over the other free options because it serves your page directly. Some
|
|
91
|
+
tunnels show an interstitial "click to continue" page on first visit, which a
|
|
92
|
+
board cannot click through — the view would render as a blank frame.
|
|
93
|
+
|
|
29
94
|
## Tailwind is already set up
|
|
30
95
|
|
|
31
96
|
Tailwind v4 is installed and configured. There is no `tailwind.config.js` to
|
package/app/dev/page.tsx
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DEVELOPER MODE — http://localhost:5173/dev
|
|
5
|
+
*
|
|
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
|
+
*
|
|
11
|
+
* WHY A WORKSPACE IS PINNED IN .env: an extension is written against a SHAPE —
|
|
12
|
+
* particular boards, particular columns — and 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.
|
|
21
|
+
*
|
|
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.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// Never prerendered: it reads .env and the network at request time.
|
|
28
|
+
export const dynamic = "force-dynamic";
|
|
29
|
+
|
|
30
|
+
const PLACEHOLDER = "Conexus_x_api_key";
|
|
31
|
+
|
|
32
|
+
interface Workspace {
|
|
33
|
+
_id: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
totalModules?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface WorkspaceMembership {
|
|
39
|
+
_id: string;
|
|
40
|
+
role?: string;
|
|
41
|
+
workspace?: Workspace | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Board {
|
|
45
|
+
_id: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
visibility?: string;
|
|
49
|
+
totalRecords?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
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(/\/$/, "");
|
|
60
|
+
|
|
61
|
+
const load = async (): Promise<DevState> => {
|
|
62
|
+
const apiKey = process.env.API_KEY;
|
|
63
|
+
const workspaceId = (process.env.WORKSPACE_ID ?? "").trim();
|
|
64
|
+
|
|
65
|
+
if (!apiKey) {
|
|
66
|
+
return {
|
|
67
|
+
kind: "error",
|
|
68
|
+
title: "No API key",
|
|
69
|
+
detail: "Add API_KEY to .env in this project, then reload.",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (apiKey === PLACEHOLDER) {
|
|
74
|
+
return {
|
|
75
|
+
kind: "error",
|
|
76
|
+
title: "The API key is still the placeholder",
|
|
77
|
+
detail: `Replace API_KEY=${PLACEHOLDER} in .env with your own key from Conexus X (Developer › API Key), then reload.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const call = async (path: string) => {
|
|
82
|
+
const response = await fetch(`${apiUrl()}${path}`, {
|
|
83
|
+
headers: { "x-api-key": apiKey },
|
|
84
|
+
cache: "no-store",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (response.status === 401) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"The API answered 401. Check the key, and that the account it belongs to is still active."
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
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) {
|
|
114
|
+
return {
|
|
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.",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const boardsBody = await call(`/modules/${workspaceId}`);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
kind: "pinned",
|
|
127
|
+
workspace: mine.workspace,
|
|
128
|
+
role: mine.role,
|
|
129
|
+
boards: (boardsBody["modules"] as Board[] | undefined) ?? [],
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return {
|
|
133
|
+
kind: "error",
|
|
134
|
+
title: "Could not reach the API",
|
|
135
|
+
detail:
|
|
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?`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export default async function DevPage() {
|
|
143
|
+
// This page reads a secret and exists only to help you build. It has no
|
|
144
|
+
// business in a deployed view, so it is not there.
|
|
145
|
+
if (process.env.NODE_ENV === "production") notFound();
|
|
146
|
+
|
|
147
|
+
const state = await load();
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<main className="mx-auto max-w-2xl p-6">
|
|
151
|
+
<header className="mb-5">
|
|
152
|
+
<h1 className="text-lg font-semibold">Developer mode</h1>
|
|
153
|
+
<p className="mt-1 text-[13px] text-muted">
|
|
154
|
+
{state.kind === "pinned"
|
|
155
|
+
? "The workspace this extension is built for, and its boards."
|
|
156
|
+
: "Reading the API key in .env."}
|
|
157
|
+
</p>
|
|
158
|
+
</header>
|
|
159
|
+
|
|
160
|
+
{state.kind === "error" ? (
|
|
161
|
+
<div className="rounded-lg border border-danger bg-danger-soft px-4 py-3 text-danger">
|
|
162
|
+
<strong className="font-semibold">{state.title}</strong>
|
|
163
|
+
<p className="mt-1.5 text-[13px] whitespace-pre-wrap">{state.detail}</p>
|
|
164
|
+
</div>
|
|
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'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
|
+
</>
|
|
210
|
+
) : (
|
|
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"}
|
|
216
|
+
</div>
|
|
217
|
+
<div className="mt-0.5 font-mono text-[12px] text-muted">
|
|
218
|
+
{state.workspace._id}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
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}
|
|
225
|
+
</span>
|
|
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
|
+
</>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
<p className="mt-5 text-[12px] text-muted">
|
|
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.
|
|
270
|
+
</p>
|
|
271
|
+
</main>
|
|
272
|
+
);
|
|
273
|
+
}
|
package/next.config.ts
CHANGED
|
@@ -19,6 +19,22 @@ const conexusOrigins =
|
|
|
19
19
|
"http://localhost:3000 https://conexus-x.vercel.app";
|
|
20
20
|
|
|
21
21
|
const nextConfig: NextConfig = {
|
|
22
|
+
/**
|
|
23
|
+
* Let a tunnel reach the dev server.
|
|
24
|
+
*
|
|
25
|
+
* Next refuses cross-origin dev requests it was not told about, which is
|
|
26
|
+
* the reason `npm run tunnel` would otherwise serve a broken page through
|
|
27
|
+
* the public URL — the HTML arrives and the dev assets behind it do not.
|
|
28
|
+
*
|
|
29
|
+
* Development only; a production build ignores this entirely.
|
|
30
|
+
*/
|
|
31
|
+
allowedDevOrigins: [
|
|
32
|
+
"*.trycloudflare.com",
|
|
33
|
+
"*.loca.lt",
|
|
34
|
+
"*.ngrok-free.app",
|
|
35
|
+
"*.ngrok.io"
|
|
36
|
+
],
|
|
37
|
+
|
|
22
38
|
/**
|
|
23
39
|
* NOT pinning `turbopack.root` here, deliberately.
|
|
24
40
|
*
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conexus-x/next-view",
|
|
3
|
-
"version": "1.0.
|
|
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": {
|
|
7
7
|
"dev": "next dev --port 5173",
|
|
8
8
|
"build": "next build",
|
|
9
9
|
"start": "next start --port 5173",
|
|
10
|
-
"lint": "eslint"
|
|
10
|
+
"lint": "eslint",
|
|
11
|
+
"tunnel": "npx -y cloudflared tunnel --url http://localhost:5173"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@conexus-x/sdk": "^0.1.0",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"postcss.config.mjs",
|
|
34
35
|
"tsconfig.json",
|
|
35
36
|
"conexus.manifest.json",
|
|
37
|
+
".env",
|
|
36
38
|
"README.md",
|
|
37
39
|
".gitignore"
|
|
38
40
|
]
|