@saasicat/ui-vue 0.26.0 → 0.27.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/LICENSE +90 -201
- package/README.md +23 -0
- package/dist/{catalog-Dch1Ryw0.d.cts → catalog-Dv2twuNi.d.cts} +205 -4
- package/dist/{catalog-Dch1Ryw0.d.ts → catalog-Dv2twuNi.d.ts} +205 -4
- package/dist/{chunk-BPF2BMCQ.js → chunk-4IZWT4UY.js} +414 -2
- package/dist/{chunk-O3J3ITF2.js → chunk-LK6YJDXC.js} +1 -1
- package/dist/{chunk-NEXTZZRQ.js → chunk-NRVF3JJJ.js} +2 -2
- package/dist/client/index.cjs +426 -5
- package/dist/client/index.d.cts +70 -4
- package/dist/client/index.d.ts +70 -4
- package/dist/client/index.js +22 -4
- package/dist/index.cjs +429 -8
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +23 -5
- package/dist/quasar/index.cjs +411 -2
- package/dist/quasar/index.d.cts +2 -2
- package/dist/quasar/index.d.ts +2 -2
- package/dist/quasar/index.js +2 -2
- package/dist/{resource-registry-D7Xjs-5f.d.cts → resource-registry-BxFpnzHY.d.cts} +1 -1
- package/dist/{resource-registry-TaR7rtq0.d.ts → resource-registry-Cn1Gy2LE.d.ts} +1 -1
- package/package.json +5 -4
- package/src/client/admin-error.ts +18 -1
- package/src/client/attach-cause.ts +21 -0
- package/src/client/nav-builder.ts +3 -1
- package/src/client/resources/bundles.resource.ts +145 -0
- package/src/client/resources/catalog.resource.ts +160 -0
- package/src/client/resources/discovery.resource.ts +108 -0
- package/src/client/resources/index.ts +52 -14
- package/src/client/resources/marketing.resource.ts +121 -0
- package/src/client/resources/promo-codes.resource.ts +63 -0
- package/src/client/resources/promotions.resource.ts +44 -0
- package/src/client/resources/subscriptions.resource.ts +19 -0
- package/src/client/resources/tenants.resource.ts +35 -9
- package/src/client/resources/users.resource.ts +31 -0
- package/src/pages-standard/SuperAdminSetupWizard.vue +7 -4
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Promo codes — the discount codes an operator hands out.
|
|
2
|
+
//
|
|
3
|
+
// Not to be confused with `promotions.resource`, which is a different
|
|
4
|
+
// endpoint (`catalog/promotions`), a different table and a different job: a
|
|
5
|
+
// promotion is a price rule attached to a plan or bundle in the catalogue, a
|
|
6
|
+
// promo code is a string a customer types at checkout. The plan's roster
|
|
7
|
+
// called both "promos"; the sources call them two things, and so do we.
|
|
8
|
+
//
|
|
9
|
+
// The list does not page. `AdminResourcesPort` answers a bare array here, so
|
|
10
|
+
// this is a plain operation over `filterQueryString` rather than a
|
|
11
|
+
// `defineListOp` — a paginated descriptor would offer a paginator over a
|
|
12
|
+
// server that ignores it.
|
|
13
|
+
|
|
14
|
+
import type { PromoCodeRecord } from '@saasicat/types';
|
|
15
|
+
|
|
16
|
+
import { defineResource, type ResourceContext } from './define-resource.js';
|
|
17
|
+
import { filterQueryString } from './list-resource.js';
|
|
18
|
+
import { requestJson } from './resource-request.js';
|
|
19
|
+
|
|
20
|
+
/** What the promo list can be narrowed by, including its "all" state. */
|
|
21
|
+
export interface PromoCodesFilter {
|
|
22
|
+
search?: string;
|
|
23
|
+
status?: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function promoCodesUrl(ctx: ResourceContext): string {
|
|
27
|
+
return `${ctx.apiBase}/promo-codes`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const promoCodesResource = defineResource('promoCodes', {
|
|
31
|
+
list: async (http, ctx, filter: PromoCodesFilter = {}): Promise<PromoCodeRecord[]> =>
|
|
32
|
+
(await requestJson<PromoCodeRecord[]>(
|
|
33
|
+
http,
|
|
34
|
+
`${promoCodesUrl(ctx)}${filterQueryString({
|
|
35
|
+
search: filter.search,
|
|
36
|
+
status: filter.status,
|
|
37
|
+
})}`,
|
|
38
|
+
)) ?? [],
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns nothing, matching `createAdminResourceClient.createPromo`.
|
|
42
|
+
*
|
|
43
|
+
* The endpoint does answer with the created record, but the page it feeds
|
|
44
|
+
* reloads the list rather than reading it, and promising a body a caller
|
|
45
|
+
* would then have to trust is a wider contract than anything holds today.
|
|
46
|
+
*/
|
|
47
|
+
create: async (http, ctx, payload: unknown): Promise<void> => {
|
|
48
|
+
await requestJson(http, promoCodesUrl(ctx), { method: 'POST', body: payload });
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
update: async (http, ctx, id: string, payload: unknown): Promise<void> => {
|
|
52
|
+
await requestJson(http, `${promoCodesUrl(ctx)}/${encodeURIComponent(id)}`, {
|
|
53
|
+
method: 'PATCH',
|
|
54
|
+
body: payload,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
remove: async (http, ctx, id: string): Promise<void> => {
|
|
59
|
+
await requestJson(http, `${promoCodesUrl(ctx)}/${encodeURIComponent(id)}`, {
|
|
60
|
+
method: 'DELETE',
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Catalogue promotions — price rules attached to a plan, bundle or offer.
|
|
2
|
+
//
|
|
3
|
+
// A sibling of `promo-codes.resource` in name only; see the note there for
|
|
4
|
+
// why the two are separate. This one lives under `catalog/`, is scoped to a
|
|
5
|
+
// project, and never reaches a customer directly.
|
|
6
|
+
|
|
7
|
+
import type { CreatePromotionData, PromotionRow, UpdatePromotionData } from '@saasicat/types';
|
|
8
|
+
|
|
9
|
+
import { defineResource, type ResourceContext } from './define-resource.js';
|
|
10
|
+
import { requestJson, requestJsonBody } from './resource-request.js';
|
|
11
|
+
|
|
12
|
+
function promotionsUrl(ctx: ResourceContext): string {
|
|
13
|
+
return `${ctx.apiBase}/catalog/promotions`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const promotionsResource = defineResource(
|
|
17
|
+
'promotions',
|
|
18
|
+
{
|
|
19
|
+
list: async (http, ctx): Promise<PromotionRow[]> =>
|
|
20
|
+
(await requestJson<PromotionRow[]>(
|
|
21
|
+
http,
|
|
22
|
+
`${promotionsUrl(ctx)}?projectKey=${encodeURIComponent(ctx.projectKey)}`,
|
|
23
|
+
)) ?? [],
|
|
24
|
+
|
|
25
|
+
create: (http, ctx, data: CreatePromotionData): Promise<PromotionRow> =>
|
|
26
|
+
requestJsonBody<PromotionRow>(http, promotionsUrl(ctx), 'Create returned no body', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
body: data,
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
update: (http, ctx, id: string, data: UpdatePromotionData): Promise<PromotionRow> =>
|
|
32
|
+
requestJsonBody<PromotionRow>(
|
|
33
|
+
http,
|
|
34
|
+
`${promotionsUrl(ctx)}/${id}`,
|
|
35
|
+
'Update returned no body',
|
|
36
|
+
{ method: 'PATCH', body: data },
|
|
37
|
+
),
|
|
38
|
+
|
|
39
|
+
remove: async (http, ctx, id: string): Promise<void> => {
|
|
40
|
+
await requestJson(http, `${promotionsUrl(ctx)}/${id}`, { method: 'DELETE' });
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{ projectScoped: true },
|
|
44
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The subscription list.
|
|
2
|
+
//
|
|
3
|
+
// One operation and no filter, because that is the endpoint: the controller
|
|
4
|
+
// takes no query and answers every subscription it can see. `SubscriptionsPage`
|
|
5
|
+
// filters and sorts the rows it was given, in the browser.
|
|
6
|
+
|
|
7
|
+
import type { AdminSubscriptionListRow } from '@saasicat/types';
|
|
8
|
+
|
|
9
|
+
import { defineResource, type ResourceContext } from './define-resource.js';
|
|
10
|
+
import { requestJson } from './resource-request.js';
|
|
11
|
+
|
|
12
|
+
function subscriptionsUrl(ctx: ResourceContext): string {
|
|
13
|
+
return `${ctx.apiBase}/subscriptions`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const subscriptionsResource = defineResource('subscriptions', {
|
|
17
|
+
list: async (http, ctx): Promise<AdminSubscriptionListRow[]> =>
|
|
18
|
+
(await requestJson<AdminSubscriptionListRow[]>(http, subscriptionsUrl(ctx))) ?? [],
|
|
19
|
+
});
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
// The tenant list.
|
|
2
|
-
//
|
|
3
|
-
// One operation, deliberately: the list `useTenants` requests today. Tenant
|
|
4
|
-
// detail, suspend and reactivate still live in `createAdminResourceClient` and
|
|
5
|
-
// move here with the page that calls them — a descriptor nothing calls has
|
|
6
|
-
// nothing keeping it honest.
|
|
1
|
+
// The tenant list, one tenant, and the two states an operator can put it in.
|
|
7
2
|
//
|
|
8
3
|
// The request pages; the controller does not. `AdminResourcesPort.listTenants`
|
|
9
4
|
// takes `status`/`plan`/`search` and answers with a bare array — no `page`, no
|
|
@@ -13,15 +8,46 @@
|
|
|
13
8
|
// has been sending, and reproducing it is the point: what the server does with
|
|
14
9
|
// the parameters is the server's to change, and a descriptor that quietly sent
|
|
15
10
|
// less would be a different request wearing the same name.
|
|
11
|
+
//
|
|
12
|
+
// `detail`, `suspend` and `reactivate` mirror `createAdminResourceClient`,
|
|
13
|
+
// which is what `TenantDetailPage` receives them through today. Slugs are
|
|
14
|
+
// encoded on the way into the path there, so they are encoded here.
|
|
16
15
|
|
|
17
|
-
import type { TenantDto, TenantListFilter } from '@saasicat/types';
|
|
16
|
+
import type { AdminTenantDetail, TenantDto, TenantListFilter } from '@saasicat/types';
|
|
18
17
|
|
|
19
|
-
import { defineResource } from './define-resource.js';
|
|
18
|
+
import { defineResource, type ResourceContext } from './define-resource.js';
|
|
20
19
|
import { defineListOp, type ListFilterOf } from './list-resource.js';
|
|
20
|
+
import { requestJson } from './resource-request.js';
|
|
21
21
|
|
|
22
22
|
/** What the tenants list can be narrowed by. The page number is not a filter. */
|
|
23
23
|
export type TenantsListFilter = ListFilterOf<TenantListFilter>;
|
|
24
24
|
|
|
25
|
+
function tenantsUrl(ctx: ResourceContext): string {
|
|
26
|
+
return `${ctx.apiBase}/tenants`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tenantUrl(ctx: ResourceContext, slug: string): string {
|
|
30
|
+
return `${tenantsUrl(ctx)}/${encodeURIComponent(slug)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
export const tenantsResource = defineResource('tenants', {
|
|
26
|
-
list: defineListOp<TenantDto, TenantsListFilter>((ctx) =>
|
|
34
|
+
list: defineListOp<TenantDto, TenantsListFilter>((ctx) => tenantsUrl(ctx)),
|
|
35
|
+
|
|
36
|
+
detail: async (http, ctx, slug: string): Promise<AdminTenantDetail | null> =>
|
|
37
|
+
requestJson<AdminTenantDetail>(http, tenantUrl(ctx, slug)),
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Suspends a tenant. The reason is recorded on the audit trail, so it is a
|
|
41
|
+
* required argument rather than an option with a default.
|
|
42
|
+
*/
|
|
43
|
+
suspend: async (http, ctx, slug: string, reason: string): Promise<void> => {
|
|
44
|
+
await requestJson(http, `${tenantUrl(ctx, slug)}/suspend`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: { reason },
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
reactivate: async (http, ctx, slug: string): Promise<void> => {
|
|
51
|
+
await requestJson(http, `${tenantUrl(ctx, slug)}/reactivate`, { method: 'POST' });
|
|
52
|
+
},
|
|
27
53
|
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The platform user list.
|
|
2
|
+
//
|
|
3
|
+
// Unpaginated, like the promo list and unlike the tenant list: the controller
|
|
4
|
+
// answers a bare array filtered by `q` and `tenant`. So this is a plain
|
|
5
|
+
// operation over `filterQueryString` rather than a `defineListOp` — see the
|
|
6
|
+
// note on `promo-codes.resource`.
|
|
7
|
+
|
|
8
|
+
import type { AdminUserListFilter, AdminUserListRow } from '@saasicat/types';
|
|
9
|
+
|
|
10
|
+
import { defineResource, type ResourceContext } from './define-resource.js';
|
|
11
|
+
import { filterQueryString } from './list-resource.js';
|
|
12
|
+
import { requestJson } from './resource-request.js';
|
|
13
|
+
|
|
14
|
+
function usersUrl(ctx: ResourceContext): string {
|
|
15
|
+
return `${ctx.apiBase}/users`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const usersResource = defineResource('users', {
|
|
19
|
+
/**
|
|
20
|
+
* `q` searches name and mail, `tenant` narrows to one tenant's members.
|
|
21
|
+
*
|
|
22
|
+
* Both are dropped when empty rather than sent as `q=` — see
|
|
23
|
+
* `isSentInQuery`; an empty search that reached the server would filter
|
|
24
|
+
* every row away on an endpoint that treats the parameter as a substring.
|
|
25
|
+
*/
|
|
26
|
+
list: async (http, ctx, filter: AdminUserListFilter = {}): Promise<AdminUserListRow[]> =>
|
|
27
|
+
(await requestJson<AdminUserListRow[]>(
|
|
28
|
+
http,
|
|
29
|
+
`${usersUrl(ctx)}${filterQueryString({ q: filter.q, tenant: filter.tenant })}`,
|
|
30
|
+
)) ?? [],
|
|
31
|
+
});
|
|
@@ -151,6 +151,7 @@
|
|
|
151
151
|
</template>
|
|
152
152
|
|
|
153
153
|
<script setup lang="ts">
|
|
154
|
+
import { attachCause } from '../client/attach-cause.js';
|
|
154
155
|
import { computed, reactive, ref } from 'vue';
|
|
155
156
|
import { SETUP_ERROR_CODES, type SetupConfirmMfaResponse, type SetupResult } from '@saasicat/types';
|
|
156
157
|
|
|
@@ -237,10 +238,12 @@ async function postJson<T>(path: string, body: unknown): Promise<T> {
|
|
|
237
238
|
if (err instanceof HttpJsonError) {
|
|
238
239
|
// The message is translated for the user; `cause` keeps the status
|
|
239
240
|
// and the machine-readable code reachable for anything upstream.
|
|
240
|
-
throw
|
|
241
|
-
(
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
throw attachCause(
|
|
242
|
+
new Error(
|
|
243
|
+
(err.code && errorByCode.value[err.code]) ||
|
|
244
|
+
formatMessage(msg.value.setup.errorHttp, { status: err.status }),
|
|
245
|
+
),
|
|
246
|
+
err,
|
|
244
247
|
);
|
|
245
248
|
}
|
|
246
249
|
throw err;
|