@carlonicora/nextjs-jsonapi 1.117.1 → 1.119.0
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/dist/{BlockNoteEditor-CEWZCORH.js → BlockNoteEditor-BPACLNMB.js} +20 -20
- package/dist/{BlockNoteEditor-CEWZCORH.js.map → BlockNoteEditor-BPACLNMB.js.map} +1 -1
- package/dist/{BlockNoteEditor-K4T6QOJO.mjs → BlockNoteEditor-CEDKBT67.mjs} +5 -5
- package/dist/JsonApiRequest-HE5LTEYK.js +26 -0
- package/dist/JsonApiRequest-HE5LTEYK.js.map +1 -0
- package/dist/{JsonApiRequest-IYL5RZ7I.mjs → JsonApiRequest-SN3DNFDR.mjs} +4 -2
- package/dist/billing/index.js +347 -347
- package/dist/billing/index.mjs +4 -4
- package/dist/{chunk-6D4GJHLK.mjs → chunk-4SPL6QH3.mjs} +277 -154
- package/dist/chunk-4SPL6QH3.mjs.map +1 -0
- package/dist/{chunk-6KLR3WJQ.js → chunk-55Q43DI5.js} +9 -9
- package/dist/{chunk-6KLR3WJQ.js.map → chunk-55Q43DI5.js.map} +1 -1
- package/dist/{chunk-WEPQD5MP.mjs → chunk-5U7QDGDQ.mjs} +3 -3
- package/dist/{chunk-S2PTWBN6.js → chunk-6P7MEU7Q.js} +64 -50
- package/dist/chunk-6P7MEU7Q.js.map +1 -0
- package/dist/{chunk-56QAXZKI.js → chunk-DXTOA7OO.js} +932 -809
- package/dist/chunk-DXTOA7OO.js.map +1 -0
- package/dist/{chunk-M7XAKLSP.mjs → chunk-FWOS6PC5.mjs} +38 -24
- package/dist/chunk-FWOS6PC5.mjs.map +1 -0
- package/dist/{chunk-J5LROFGL.js → chunk-RCMIVTVL.js} +14 -11
- package/dist/chunk-RCMIVTVL.js.map +1 -0
- package/dist/{chunk-KK7PYKVI.mjs → chunk-WC2BTKJP.mjs} +13 -10
- package/dist/{chunk-KK7PYKVI.mjs.map → chunk-WC2BTKJP.mjs.map} +1 -1
- package/dist/client/index.d.mts +15 -1
- package/dist/client/index.d.ts +15 -1
- package/dist/client/index.js +7 -5
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +6 -4
- package/dist/components/index.d.mts +72 -30
- package/dist/components/index.d.ts +72 -30
- package/dist/components/index.js +7 -5
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +6 -4
- package/dist/contexts/index.js +5 -5
- package/dist/contexts/index.mjs +4 -4
- package/dist/core/index.d.mts +7 -2
- package/dist/core/index.d.ts +7 -2
- package/dist/core/index.js +3 -3
- package/dist/core/index.mjs +2 -2
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +4 -4
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/index.mjs +3 -3
- package/dist/{s3.service-CihgBy97.d.mts → s3.service-CBqyDZyz.d.mts} +7 -0
- package/dist/{s3.service-Cyvfk3qV.d.ts → s3.service-gcEjq8gF.d.ts} +7 -0
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +12 -12
- package/dist/server/index.mjs +2 -2
- package/package.json +1 -1
- package/src/client/JsonApiClient.ts +23 -8
- package/src/client/__tests__/JsonApiClient.test.ts +31 -0
- package/src/components/containers/RoundPageContainer.tsx +197 -76
- package/src/components/containers/TabsContainer.tsx +14 -0
- package/src/components/containers/__tests__/partitionTabs.spec.ts +51 -0
- package/src/components/containers/index.ts +1 -0
- package/src/components/containers/partitionTabs.ts +43 -0
- package/src/components/forms/EditorSheet.tsx +7 -2
- package/src/core/abstracts/AbstractService.ts +13 -0
- package/src/core/abstracts/ClientAbstractService.ts +10 -0
- package/src/unified/JsonApiRequest.ts +22 -8
- package/src/unified/__tests__/JsonApiRequest.test.ts +29 -0
- package/dist/JsonApiRequest-OSGJ63XR.js +0 -24
- package/dist/JsonApiRequest-OSGJ63XR.js.map +0 -1
- package/dist/chunk-56QAXZKI.js.map +0 -1
- package/dist/chunk-6D4GJHLK.mjs.map +0 -1
- package/dist/chunk-J5LROFGL.js.map +0 -1
- package/dist/chunk-M7XAKLSP.mjs.map +0 -1
- package/dist/chunk-S2PTWBN6.js.map +0 -1
- /package/dist/{BlockNoteEditor-K4T6QOJO.mjs.map → BlockNoteEditor-CEDKBT67.mjs.map} +0 -0
- /package/dist/{JsonApiRequest-IYL5RZ7I.mjs.map → JsonApiRequest-SN3DNFDR.mjs.map} +0 -0
- /package/dist/{chunk-WEPQD5MP.mjs.map → chunk-5U7QDGDQ.mjs.map} +0 -0
package/dist/server/index.js
CHANGED
|
@@ -15,13 +15,13 @@ var _chunkIOLU4ASDjs = require('../chunk-IOLU4ASD.js');
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
var
|
|
18
|
+
var _chunk6P7MEU7Qjs = require('../chunk-6P7MEU7Q.js');
|
|
19
19
|
require('../chunk-LXKSUWAV.js');
|
|
20
20
|
require('../chunk-IBS6NI7D.js');
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
var
|
|
24
|
+
var _chunkRCMIVTVLjs = require('../chunk-RCMIVTVL.js');
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
var _chunk7QVYU63Ejs = require('../chunk-7QVYU63E.js');
|
|
@@ -86,7 +86,7 @@ var ServerSession = class {
|
|
|
86
86
|
if (!rawModules) return false;
|
|
87
87
|
const modules = JSON.parse(_pako2.default.ungzip(Buffer.from(rawModules, "base64"), { to: "string" }));
|
|
88
88
|
const selectedModule = modules.find((module) => module.id === params.module.moduleId);
|
|
89
|
-
return
|
|
89
|
+
return _chunk6P7MEU7Qjs.checkPermissionsFromServer.call(void 0, {
|
|
90
90
|
module: params.module,
|
|
91
91
|
action: params.action,
|
|
92
92
|
data: params.data,
|
|
@@ -158,7 +158,7 @@ async function ServerJsonApiGet(params) {
|
|
|
158
158
|
language: params.language,
|
|
159
159
|
additionalHeaders: _optionalChain([_serverConfig, 'optionalAccess', _23 => _23.additionalHeaders])
|
|
160
160
|
});
|
|
161
|
-
return
|
|
161
|
+
return _chunkRCMIVTVLjs.translateResponse.call(void 0, {
|
|
162
162
|
classKey: params.classKey,
|
|
163
163
|
apiResponse,
|
|
164
164
|
companyId: params.companyId,
|
|
@@ -179,7 +179,7 @@ async function ServerJsonApiPost(params) {
|
|
|
179
179
|
if (!body) {
|
|
180
180
|
body = {};
|
|
181
181
|
} else if (params.overridesJsonApiCreation !== true) {
|
|
182
|
-
body =
|
|
182
|
+
body = _chunkRCMIVTVLjs.JsonApiDataFactory.create(params.classKey, body);
|
|
183
183
|
}
|
|
184
184
|
const apiResponse = await _chunkIOLU4ASDjs.serverRequest.call(void 0, {
|
|
185
185
|
method: "POST",
|
|
@@ -191,7 +191,7 @@ async function ServerJsonApiPost(params) {
|
|
|
191
191
|
language: params.language,
|
|
192
192
|
additionalHeaders: _optionalChain([_serverConfig, 'optionalAccess', _24 => _24.additionalHeaders])
|
|
193
193
|
});
|
|
194
|
-
return
|
|
194
|
+
return _chunkRCMIVTVLjs.translateResponse.call(void 0, {
|
|
195
195
|
classKey: _nullishCoalesce(params.responseType, () => ( params.classKey)),
|
|
196
196
|
apiResponse,
|
|
197
197
|
companyId: params.companyId,
|
|
@@ -206,7 +206,7 @@ async function ServerJsonApiPut(params) {
|
|
|
206
206
|
if (!body) {
|
|
207
207
|
body = {};
|
|
208
208
|
} else {
|
|
209
|
-
body =
|
|
209
|
+
body = _chunkRCMIVTVLjs.JsonApiDataFactory.create(params.classKey, body);
|
|
210
210
|
}
|
|
211
211
|
const apiResponse = await _chunkIOLU4ASDjs.serverRequest.call(void 0, {
|
|
212
212
|
method: "PUT",
|
|
@@ -218,7 +218,7 @@ async function ServerJsonApiPut(params) {
|
|
|
218
218
|
language: params.language,
|
|
219
219
|
additionalHeaders: _optionalChain([_serverConfig, 'optionalAccess', _25 => _25.additionalHeaders])
|
|
220
220
|
});
|
|
221
|
-
return
|
|
221
|
+
return _chunkRCMIVTVLjs.translateResponse.call(void 0, {
|
|
222
222
|
classKey: _nullishCoalesce(params.responseType, () => ( params.classKey)),
|
|
223
223
|
apiResponse,
|
|
224
224
|
companyId: params.companyId,
|
|
@@ -233,7 +233,7 @@ async function ServerJsonApiPatch(params) {
|
|
|
233
233
|
if (!body) {
|
|
234
234
|
body = {};
|
|
235
235
|
} else if (params.overridesJsonApiCreation !== true) {
|
|
236
|
-
body =
|
|
236
|
+
body = _chunkRCMIVTVLjs.JsonApiDataFactory.create(params.classKey, body);
|
|
237
237
|
}
|
|
238
238
|
const apiResponse = await _chunkIOLU4ASDjs.serverRequest.call(void 0, {
|
|
239
239
|
method: "PATCH",
|
|
@@ -245,7 +245,7 @@ async function ServerJsonApiPatch(params) {
|
|
|
245
245
|
language: params.language,
|
|
246
246
|
additionalHeaders: _optionalChain([_serverConfig, 'optionalAccess', _26 => _26.additionalHeaders])
|
|
247
247
|
});
|
|
248
|
-
return
|
|
248
|
+
return _chunkRCMIVTVLjs.translateResponse.call(void 0, {
|
|
249
249
|
classKey: _nullishCoalesce(params.responseType, () => ( params.classKey)),
|
|
250
250
|
apiResponse,
|
|
251
251
|
companyId: params.companyId,
|
|
@@ -264,7 +264,7 @@ async function ServerJsonApiDelete(params) {
|
|
|
264
264
|
language: params.language,
|
|
265
265
|
additionalHeaders: _optionalChain([_serverConfig, 'optionalAccess', _27 => _27.additionalHeaders])
|
|
266
266
|
});
|
|
267
|
-
return
|
|
267
|
+
return _chunkRCMIVTVLjs.translateResponse.call(void 0, {
|
|
268
268
|
classKey: _nullishCoalesce(params.responseType, () => ( params.classKey)),
|
|
269
269
|
apiResponse,
|
|
270
270
|
companyId: params.companyId,
|
|
@@ -296,5 +296,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, ServerJsonApiDelete, "ServerJsonApiDelete")
|
|
|
296
296
|
|
|
297
297
|
|
|
298
298
|
|
|
299
|
-
exports.ServerAuthService =
|
|
299
|
+
exports.ServerAuthService = _chunk6P7MEU7Qjs.AuthService; exports.ServerCompanyService = _chunk6P7MEU7Qjs.CompanyService; exports.ServerContentService = _chunk6P7MEU7Qjs.ContentService; exports.ServerFeatureService = _chunk6P7MEU7Qjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunk6P7MEU7Qjs.NotificationService; exports.ServerPushService = _chunk6P7MEU7Qjs.PushService; exports.ServerRoleService = _chunk6P7MEU7Qjs.RoleService; exports.ServerS3Service = _chunk6P7MEU7Qjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunk6P7MEU7Qjs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunkIOLU4ASDjs.serverRequest;
|
|
300
300
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.mjs
CHANGED
|
@@ -15,13 +15,13 @@ import {
|
|
|
15
15
|
S3Service,
|
|
16
16
|
UserService,
|
|
17
17
|
checkPermissionsFromServer
|
|
18
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-FWOS6PC5.mjs";
|
|
19
19
|
import "../chunk-AUXK7QSA.mjs";
|
|
20
20
|
import "../chunk-C7C7VY4F.mjs";
|
|
21
21
|
import {
|
|
22
22
|
JsonApiDataFactory,
|
|
23
23
|
translateResponse
|
|
24
|
-
} from "../chunk-
|
|
24
|
+
} from "../chunk-WC2BTKJP.mjs";
|
|
25
25
|
import {
|
|
26
26
|
__name
|
|
27
27
|
} from "../chunk-PAWJFY3S.mjs";
|
package/package.json
CHANGED
|
@@ -72,9 +72,18 @@ function runClientBootstrapper(): void {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the final request URL for an endpoint (client-side variant of `buildUrl`
|
|
77
|
+
* in `unified/JsonApiRequest.ts` — kept in lockstep with it).
|
|
78
|
+
*
|
|
79
|
+
* - `endpoint` starting with "http" is always passed through unchanged (existing behaviour).
|
|
80
|
+
* - Otherwise, an explicit `baseUrl` (per-call override) takes precedence over the global
|
|
81
|
+
* `getClientApiUrl()` resolution. Omitting `baseUrl` preserves today's behaviour exactly.
|
|
82
|
+
*/
|
|
83
|
+
export function buildClientUrl(endpoint: string, baseUrl?: string): string {
|
|
84
|
+
if (endpoint.startsWith("http")) return endpoint;
|
|
85
|
+
const apiUrl = baseUrl ?? getClientApiUrl();
|
|
86
|
+
return `${apiUrl}${endpoint}`;
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
export async function ClientJsonApiGet(params: {
|
|
@@ -82,13 +91,14 @@ export async function ClientJsonApiGet(params: {
|
|
|
82
91
|
endpoint: string;
|
|
83
92
|
companyId?: string;
|
|
84
93
|
language: string;
|
|
94
|
+
baseUrl?: string;
|
|
85
95
|
}): Promise<ApiResponseInterface> {
|
|
86
96
|
runClientBootstrapper();
|
|
87
97
|
const token = await getClientToken();
|
|
88
98
|
|
|
89
99
|
const apiResponse = await directFetch({
|
|
90
100
|
method: "GET",
|
|
91
|
-
url: buildClientUrl(params.endpoint),
|
|
101
|
+
url: buildClientUrl(params.endpoint, params.baseUrl),
|
|
92
102
|
token,
|
|
93
103
|
companyId: params.companyId,
|
|
94
104
|
language: params.language,
|
|
@@ -106,6 +116,7 @@ export async function ClientJsonApiGet(params: {
|
|
|
106
116
|
endpoint,
|
|
107
117
|
companyId: params.companyId,
|
|
108
118
|
language: params.language,
|
|
119
|
+
baseUrl: params.baseUrl,
|
|
109
120
|
}),
|
|
110
121
|
});
|
|
111
122
|
}
|
|
@@ -119,6 +130,7 @@ export async function ClientJsonApiPost(params: {
|
|
|
119
130
|
files?: { [key: string]: File | Blob } | File | Blob;
|
|
120
131
|
language: string;
|
|
121
132
|
responseType?: ApiRequestDataTypeInterface;
|
|
133
|
+
baseUrl?: string;
|
|
122
134
|
}): Promise<ApiResponseInterface> {
|
|
123
135
|
runClientBootstrapper();
|
|
124
136
|
const token = await getClientToken();
|
|
@@ -132,7 +144,7 @@ export async function ClientJsonApiPost(params: {
|
|
|
132
144
|
|
|
133
145
|
const apiResponse = await directFetch({
|
|
134
146
|
method: "POST",
|
|
135
|
-
url: buildClientUrl(params.endpoint),
|
|
147
|
+
url: buildClientUrl(params.endpoint, params.baseUrl),
|
|
136
148
|
token,
|
|
137
149
|
body,
|
|
138
150
|
files: params.files,
|
|
@@ -157,6 +169,7 @@ export async function ClientJsonApiPut(params: {
|
|
|
157
169
|
files?: { [key: string]: File | Blob } | File | Blob;
|
|
158
170
|
language: string;
|
|
159
171
|
responseType?: ApiRequestDataTypeInterface;
|
|
172
|
+
baseUrl?: string;
|
|
160
173
|
}): Promise<ApiResponseInterface> {
|
|
161
174
|
runClientBootstrapper();
|
|
162
175
|
const token = await getClientToken();
|
|
@@ -170,7 +183,7 @@ export async function ClientJsonApiPut(params: {
|
|
|
170
183
|
|
|
171
184
|
const apiResponse = await directFetch({
|
|
172
185
|
method: "PUT",
|
|
173
|
-
url: buildClientUrl(params.endpoint),
|
|
186
|
+
url: buildClientUrl(params.endpoint, params.baseUrl),
|
|
174
187
|
token,
|
|
175
188
|
body,
|
|
176
189
|
files: params.files,
|
|
@@ -196,6 +209,7 @@ export async function ClientJsonApiPatch(params: {
|
|
|
196
209
|
overridesJsonApiCreation?: boolean;
|
|
197
210
|
responseType?: ApiRequestDataTypeInterface;
|
|
198
211
|
language: string;
|
|
212
|
+
baseUrl?: string;
|
|
199
213
|
}): Promise<ApiResponseInterface> {
|
|
200
214
|
runClientBootstrapper();
|
|
201
215
|
const token = await getClientToken();
|
|
@@ -209,7 +223,7 @@ export async function ClientJsonApiPatch(params: {
|
|
|
209
223
|
|
|
210
224
|
const apiResponse = await directFetch({
|
|
211
225
|
method: "PATCH",
|
|
212
|
-
url: buildClientUrl(params.endpoint),
|
|
226
|
+
url: buildClientUrl(params.endpoint, params.baseUrl),
|
|
213
227
|
token,
|
|
214
228
|
body,
|
|
215
229
|
files: params.files,
|
|
@@ -232,13 +246,14 @@ export async function ClientJsonApiDelete(params: {
|
|
|
232
246
|
companyId?: string;
|
|
233
247
|
language: string;
|
|
234
248
|
responseType?: ApiRequestDataTypeInterface;
|
|
249
|
+
baseUrl?: string;
|
|
235
250
|
}): Promise<ApiResponseInterface> {
|
|
236
251
|
runClientBootstrapper();
|
|
237
252
|
const token = await getClientToken();
|
|
238
253
|
|
|
239
254
|
const apiResponse = await directFetch({
|
|
240
255
|
method: "DELETE",
|
|
241
|
-
url: buildClientUrl(params.endpoint),
|
|
256
|
+
url: buildClientUrl(params.endpoint, params.baseUrl),
|
|
242
257
|
token,
|
|
243
258
|
companyId: params.companyId,
|
|
244
259
|
language: params.language,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { buildClientUrl } from "../JsonApiClient";
|
|
3
|
+
|
|
4
|
+
describe("buildClientUrl", () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.unstubAllEnvs();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("uses NEXT_PUBLIC_API_URL when baseUrl is omitted", () => {
|
|
10
|
+
vi.stubEnv("NEXT_PUBLIC_API_URL", "https://api.example.com");
|
|
11
|
+
|
|
12
|
+
expect(buildClientUrl("/sncass/sync")).toBe("https://api.example.com/sncass/sync");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("uses the per-call baseUrl override when provided", () => {
|
|
16
|
+
vi.stubEnv("NEXT_PUBLIC_API_URL", "https://api.example.com");
|
|
17
|
+
|
|
18
|
+
expect(buildClientUrl("/sncass/sync", "https://corpus.example.com")).toBe("https://corpus.example.com/sncass/sync");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("passes through an http-prefixed endpoint regardless of baseUrl", () => {
|
|
22
|
+
vi.stubEnv("NEXT_PUBLIC_API_URL", "https://api.example.com");
|
|
23
|
+
|
|
24
|
+
expect(buildClientUrl("https://elsewhere.example.com/sncass/sync", "https://corpus.example.com")).toBe(
|
|
25
|
+
"https://elsewhere.example.com/sncass/sync",
|
|
26
|
+
);
|
|
27
|
+
expect(buildClientUrl("https://elsewhere.example.com/sncass/sync")).toBe(
|
|
28
|
+
"https://elsewhere.example.com/sncass/sync",
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import {
|
|
4
4
|
Select,
|
|
5
5
|
SelectContent,
|
|
6
|
+
SelectGroup,
|
|
6
7
|
SelectItem,
|
|
8
|
+
SelectLabel,
|
|
7
9
|
SelectTrigger,
|
|
8
10
|
SelectValue,
|
|
9
11
|
Sheet,
|
|
@@ -15,7 +17,7 @@ import {
|
|
|
15
17
|
TabsList,
|
|
16
18
|
TabsTrigger,
|
|
17
19
|
} from "@/components";
|
|
18
|
-
import { Tab } from "@/components/containers";
|
|
20
|
+
import { partitionTabs, Tab } from "@/components/containers";
|
|
19
21
|
import { RoundPageContainerTitle } from "@/components/containers/RoundPageContainerTitle";
|
|
20
22
|
import { Header } from "@/components/navigations";
|
|
21
23
|
import { useHeaderChildren, useHeaderLeftContent } from "@/contexts";
|
|
@@ -23,7 +25,7 @@ import { useUrlRewriter } from "@/hooks";
|
|
|
23
25
|
import { cn, useIsMobile } from "@/index";
|
|
24
26
|
import { ModuleWithPermissions } from "@/permissions";
|
|
25
27
|
import { useSearchParams } from "next/navigation";
|
|
26
|
-
import { ReactNode, useCallback, useEffect, useState } from "react";
|
|
28
|
+
import { Fragment, ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
|
27
29
|
|
|
28
30
|
const DETAILS_COOKIE_NAME = "round_page_details_state";
|
|
29
31
|
const DETAILS_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|
@@ -37,8 +39,31 @@ type RoundPageContainerProps = {
|
|
|
37
39
|
fullWidth?: boolean;
|
|
38
40
|
forceHeader?: boolean;
|
|
39
41
|
header?: ReactNode;
|
|
42
|
+
/**
|
|
43
|
+
* Section-navigation layout for `tabs`.
|
|
44
|
+
* - `"tabs"` (default) — horizontal `TabsList` (desktop) / `Select` (mobile).
|
|
45
|
+
* Unchanged from prior behaviour; existing callers need no edits.
|
|
46
|
+
* - `"rail"` — vertical 220px left rail grouped by each tab's `group`, with a
|
|
47
|
+
* `<md` grouped `Select` fallback. Mirrors the retired
|
|
48
|
+
* `PageContainerContentDetails` rail.
|
|
49
|
+
*/
|
|
50
|
+
layout?: "tabs" | "rail";
|
|
40
51
|
};
|
|
41
52
|
|
|
53
|
+
// Rail trigger class: override the horizontal TabsTrigger defaults for a
|
|
54
|
+
// vertical, left-aligned, dark-filled active state. tailwind-merge inside cn()
|
|
55
|
+
// resolves the conflicts with the base classes.
|
|
56
|
+
const railTriggerClass = cn(
|
|
57
|
+
"flex w-full items-center justify-start rounded-md px-3 py-1.5 text-left text-sm leading-tight whitespace-normal",
|
|
58
|
+
"text-muted-foreground",
|
|
59
|
+
"hover:bg-muted hover:text-foreground",
|
|
60
|
+
"data-[state=active]:bg-foreground data-[state=active]:text-background",
|
|
61
|
+
"data-[state=active]:font-semibold data-[state=active]:shadow-none",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/** Stable value for the URL `?section=` and active-tab matching. */
|
|
65
|
+
const tabValue = (tab: Tab): string => tab.sectionKey ?? tab.key?.name ?? tab.label;
|
|
66
|
+
|
|
42
67
|
export function RoundPageContainer({
|
|
43
68
|
module,
|
|
44
69
|
id,
|
|
@@ -48,6 +73,7 @@ export function RoundPageContainer({
|
|
|
48
73
|
fullWidth,
|
|
49
74
|
forceHeader,
|
|
50
75
|
header,
|
|
76
|
+
layout = "tabs",
|
|
51
77
|
}: RoundPageContainerProps) {
|
|
52
78
|
const headerChildren = useHeaderChildren();
|
|
53
79
|
const headerLeftContent = useHeaderLeftContent();
|
|
@@ -75,15 +101,14 @@ export function RoundPageContainer({
|
|
|
75
101
|
const rewriteUrl = useUrlRewriter();
|
|
76
102
|
|
|
77
103
|
const initialValue = tabs
|
|
78
|
-
? (section && tabs.find((i) => (i
|
|
79
|
-
(tabs[0].key?.name ?? tabs[0].label)
|
|
104
|
+
? (section && tabs.find((i) => tabValue(i) === section) ? section : null) || tabValue(tabs[0])
|
|
80
105
|
: undefined;
|
|
81
106
|
|
|
82
107
|
const [activeTab, setActiveTab] = useState(initialValue);
|
|
83
108
|
|
|
84
109
|
useEffect(() => {
|
|
85
110
|
if (tabs && section) {
|
|
86
|
-
const tab = tabs.find((i) => (i
|
|
111
|
+
const tab = tabs.find((i) => tabValue(i) === section);
|
|
87
112
|
if (tab) {
|
|
88
113
|
setActiveTab(section);
|
|
89
114
|
}
|
|
@@ -98,7 +123,10 @@ export function RoundPageContainer({
|
|
|
98
123
|
[module, id, rewriteUrl],
|
|
99
124
|
);
|
|
100
125
|
|
|
101
|
-
const activeFillHeight = tabs?.find((t) => (t
|
|
126
|
+
const activeFillHeight = tabs?.find((t) => tabValue(t) === activeTab)?.fillHeight === true;
|
|
127
|
+
|
|
128
|
+
// Rail partition — only consumed by `layout="rail"` but cheap to compute.
|
|
129
|
+
const { ungrouped, groups } = useMemo(() => partitionTabs(tabs ?? []), [tabs]);
|
|
102
130
|
|
|
103
131
|
const isReady = mounted && isMobile !== undefined;
|
|
104
132
|
|
|
@@ -135,85 +163,178 @@ export function RoundPageContainer({
|
|
|
135
163
|
/>
|
|
136
164
|
)}
|
|
137
165
|
<div className="flex h-full w-full overflow-hidden">
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
166
|
+
{layout === "rail" && tabs ? (
|
|
167
|
+
// Rail layout: the vertical-tab navigation is a flush-left
|
|
168
|
+
// sidebar of the card and the content fills the full remaining
|
|
169
|
+
// width (like `fullWidth`) — NOT the centred max-w-6xl column.
|
|
170
|
+
<Tabs
|
|
171
|
+
value={activeTab}
|
|
172
|
+
onValueChange={handleTabChange}
|
|
173
|
+
orientation="vertical"
|
|
174
|
+
className="flex h-full min-w-0 grow overflow-hidden"
|
|
175
|
+
>
|
|
176
|
+
{/* Flush-left section rail — md and up */}
|
|
177
|
+
<aside
|
|
178
|
+
data-testid="round-page-rail"
|
|
179
|
+
className="hidden shrink-0 border-r p-4 md:flex md:w-56 md:flex-col md:overflow-y-auto"
|
|
180
|
+
>
|
|
181
|
+
<TabsList className="flex h-auto flex-col items-stretch gap-0.5 bg-transparent p-0">
|
|
182
|
+
{ungrouped.map((tab) => (
|
|
183
|
+
<TabsTrigger key={tab.label} value={tabValue(tab)} className={railTriggerClass}>
|
|
184
|
+
{tab.contentLabel ?? tab.label}
|
|
185
|
+
</TabsTrigger>
|
|
186
|
+
))}
|
|
187
|
+
{groups.map((group) => (
|
|
188
|
+
<Fragment key={group.label}>
|
|
189
|
+
<div
|
|
190
|
+
role="presentation"
|
|
191
|
+
className="text-muted-foreground px-3 pt-3 pb-1 text-[10px] font-bold tracking-wider uppercase"
|
|
192
|
+
>
|
|
193
|
+
{group.label}
|
|
194
|
+
</div>
|
|
195
|
+
{group.items.map((tab) => (
|
|
196
|
+
<TabsTrigger key={tab.label} value={tabValue(tab)} className={railTriggerClass}>
|
|
197
|
+
{tab.contentLabel ?? tab.label}
|
|
198
|
+
</TabsTrigger>
|
|
199
|
+
))}
|
|
200
|
+
</Fragment>
|
|
201
|
+
))}
|
|
202
|
+
</TabsList>
|
|
203
|
+
</aside>
|
|
204
|
+
|
|
205
|
+
{/* Content — full width, fills the remaining space */}
|
|
206
|
+
<div className="flex min-w-0 grow flex-col overflow-hidden">
|
|
207
|
+
{/* Section Select — below md */}
|
|
208
|
+
<div data-testid="round-page-rail-select" className="p-2 md:hidden">
|
|
209
|
+
<Select
|
|
210
|
+
value={activeTab}
|
|
211
|
+
onValueChange={(value) => {
|
|
212
|
+
if (value) handleTabChange(value);
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
<SelectTrigger className="w-full">
|
|
216
|
+
<SelectValue />
|
|
217
|
+
</SelectTrigger>
|
|
218
|
+
<SelectContent>
|
|
219
|
+
{ungrouped.map((tab) => (
|
|
220
|
+
<SelectItem key={tab.label} value={tabValue(tab)}>
|
|
221
|
+
{tab.contentLabel ?? tab.label}
|
|
222
|
+
</SelectItem>
|
|
223
|
+
))}
|
|
224
|
+
{groups.map((group) => (
|
|
225
|
+
<SelectGroup key={group.label}>
|
|
226
|
+
<SelectLabel>{group.label}</SelectLabel>
|
|
227
|
+
{group.items.map((tab) => (
|
|
228
|
+
<SelectItem key={tab.label} value={tabValue(tab)}>
|
|
229
|
+
{tab.contentLabel ?? tab.label}
|
|
230
|
+
</SelectItem>
|
|
231
|
+
))}
|
|
232
|
+
</SelectGroup>
|
|
233
|
+
))}
|
|
234
|
+
</SelectContent>
|
|
235
|
+
</Select>
|
|
236
|
+
</div>
|
|
237
|
+
<div
|
|
238
|
+
className={cn(
|
|
239
|
+
`min-w-0 grow`,
|
|
240
|
+
activeFillHeight ? `flex flex-col overflow-hidden` : `overflow-y-auto`,
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
243
|
+
{header}
|
|
244
|
+
{tabs.map((tab) => (
|
|
245
|
+
<TabsContent
|
|
246
|
+
key={tab.label}
|
|
247
|
+
value={tabValue(tab)}
|
|
248
|
+
className={tab.fillHeight ? `flex min-h-0 w-full flex-1 flex-col` : ``}
|
|
249
|
+
>
|
|
250
|
+
{tab.content}
|
|
251
|
+
</TabsContent>
|
|
252
|
+
))}
|
|
253
|
+
{children && <div className="flex">{children}</div>}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</Tabs>
|
|
257
|
+
) : (
|
|
146
258
|
<div
|
|
147
259
|
className={cn(
|
|
148
|
-
`
|
|
149
|
-
|
|
150
|
-
|
|
260
|
+
`grow`,
|
|
261
|
+
isMobile ? `p-2` : `p-4`,
|
|
262
|
+
activeFillHeight ? `flex flex-col overflow-hidden` : `overflow-y-auto`,
|
|
263
|
+
fullWidth && `p-0`,
|
|
151
264
|
)}
|
|
152
265
|
>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
266
|
+
<div
|
|
267
|
+
className={cn(
|
|
268
|
+
`mx-auto max-w-6xl space-y-8`,
|
|
269
|
+
activeFillHeight && `flex w-full flex-1 min-h-0 flex-col space-y-0`,
|
|
270
|
+
fullWidth && `max-w-full w-full p-0 h-full`,
|
|
271
|
+
)}
|
|
272
|
+
>
|
|
273
|
+
{header}
|
|
274
|
+
{tabs ? (
|
|
275
|
+
<>
|
|
276
|
+
<Tabs
|
|
277
|
+
value={activeTab}
|
|
278
|
+
className={cn(`w-full`, activeFillHeight && `flex flex-1 min-h-0 flex-col`)}
|
|
279
|
+
onValueChange={handleTabChange}
|
|
280
|
+
>
|
|
281
|
+
{isMobile ? (
|
|
282
|
+
<div className="p-0">
|
|
283
|
+
<Select
|
|
284
|
+
value={activeTab}
|
|
285
|
+
onValueChange={(value) => {
|
|
286
|
+
if (value) handleTabChange(value);
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
<SelectTrigger className="w-full">
|
|
290
|
+
<SelectValue />
|
|
291
|
+
</SelectTrigger>
|
|
292
|
+
<SelectContent>
|
|
293
|
+
{tabs.map((tab) => (
|
|
294
|
+
<SelectItem key={tab.label} value={tabValue(tab)}>
|
|
295
|
+
{tab.contentLabel ?? tab.label}
|
|
296
|
+
</SelectItem>
|
|
297
|
+
))}
|
|
298
|
+
</SelectContent>
|
|
299
|
+
</Select>
|
|
300
|
+
</div>
|
|
301
|
+
) : (
|
|
302
|
+
<div className="p-4">
|
|
303
|
+
<TabsList>
|
|
173
304
|
{tabs.map((tab) => (
|
|
174
|
-
<
|
|
305
|
+
<TabsTrigger key={tab.label} value={tabValue(tab)} className="px-4">
|
|
175
306
|
{tab.contentLabel ?? tab.label}
|
|
176
|
-
</
|
|
307
|
+
</TabsTrigger>
|
|
177
308
|
))}
|
|
178
|
-
</
|
|
179
|
-
</
|
|
180
|
-
</div>
|
|
181
|
-
) : (
|
|
182
|
-
<div className="p-4">
|
|
183
|
-
<TabsList>
|
|
184
|
-
{tabs.map((tab) => (
|
|
185
|
-
<TabsTrigger key={tab.label} value={tab.key?.name ?? tab.label} className="px-4">
|
|
186
|
-
{tab.contentLabel ?? tab.label}
|
|
187
|
-
</TabsTrigger>
|
|
188
|
-
))}
|
|
189
|
-
</TabsList>
|
|
190
|
-
</div>
|
|
191
|
-
)}
|
|
192
|
-
<div
|
|
193
|
-
className={cn(
|
|
194
|
-
`flex w-full `,
|
|
195
|
-
isMobile ? `` : `px-4`,
|
|
196
|
-
activeFillHeight ? `flex-1 min-h-0` : `overflow-y-auto`,
|
|
309
|
+
</TabsList>
|
|
310
|
+
</div>
|
|
197
311
|
)}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
312
|
+
<div
|
|
313
|
+
className={cn(
|
|
314
|
+
`flex w-full `,
|
|
315
|
+
isMobile ? `` : `px-4`,
|
|
316
|
+
activeFillHeight ? `flex-1 min-h-0` : `overflow-y-auto`,
|
|
317
|
+
)}
|
|
318
|
+
>
|
|
319
|
+
{tabs.map((tab) => (
|
|
320
|
+
<TabsContent
|
|
321
|
+
key={tab.label}
|
|
322
|
+
value={tabValue(tab)}
|
|
323
|
+
className={tab.fillHeight ? `flex flex-1 min-h-0 w-full flex-col` : `pb-20`}
|
|
324
|
+
>
|
|
325
|
+
{tab.content}
|
|
326
|
+
</TabsContent>
|
|
327
|
+
))}
|
|
328
|
+
</div>
|
|
329
|
+
</Tabs>
|
|
330
|
+
{children && <div className={cn(`flex`, isMobile ? `px-2` : `px-4`)}>{children}</div>}
|
|
331
|
+
</>
|
|
332
|
+
) : (
|
|
333
|
+
children
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
215
336
|
</div>
|
|
216
|
-
|
|
337
|
+
)}
|
|
217
338
|
{details &&
|
|
218
339
|
(isMobile ? (
|
|
219
340
|
<Sheet open={showDetails} onOpenChange={setShowDetails}>
|
|
@@ -13,6 +13,20 @@ export type Tab = {
|
|
|
13
13
|
content: React.ReactNode;
|
|
14
14
|
modules?: ModuleWithPermissions[];
|
|
15
15
|
action?: Action;
|
|
16
|
+
/**
|
|
17
|
+
* Section header this tab clusters under in the `RoundPageContainer`
|
|
18
|
+
* `layout="rail"` navigation. Tabs without a `group` render at the top of the
|
|
19
|
+
* rail (the "pinned" stratum) in declared order; groups render in first-seen
|
|
20
|
+
* order. Ignored by the default `layout="tabs"` (flat) rendering.
|
|
21
|
+
*/
|
|
22
|
+
group?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Stable identifier used for the URL `?section=` value and active-tab
|
|
25
|
+
* matching. Defaults to `key?.name ?? label`. Provide this for tabs that have
|
|
26
|
+
* no `key` (no backing module) but still need a stable, locale-independent
|
|
27
|
+
* deep link — e.g. `"dashboard"`, `"analysis"`, `"timeline"`.
|
|
28
|
+
*/
|
|
29
|
+
sectionKey?: string;
|
|
16
30
|
/**
|
|
17
31
|
* When true, the tab content fills the available viewport height inside its
|
|
18
32
|
* container. The tab's wrapper chain switches to a flex-column layout with
|