@k2b/cloud 0.43.0 → 0.45.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/package.json +11 -2
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +158 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +7 -5
- package/src/_internal/define-app.ts +57 -6
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +11 -1
- package/src/_internal/registry.ts +8 -1
- package/src/_internal/runtime-context.ts +8 -1
- package/src/access/AppAccessEditor.tsx +2 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +20 -5
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/PrincipalPicker.tsx +14 -4
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-messages.ts +6 -0
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +7 -0
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +10 -6
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +40 -25
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +12 -5
- package/src/ai/inference-calls.ts +5 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api.ts +26 -9
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +58 -47
- package/src/ai/projects.ts +110 -88
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +55 -6
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider.ts +1 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +4 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +9 -3
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +47 -48
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +21 -44
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +27 -22
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +22 -3
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-skills.ts +4 -1
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +35 -5
- package/src/api/capabilities.ts +28 -4
- package/src/api/index.ts +7 -1
- package/src/api/mcp.ts +175 -2
- package/src/api/me.ts +6 -4
- package/src/api/pwa-phone.ts +75 -26
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/app-pwa/contracts.ts +4 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +1 -0
- package/src/app-pwa/service.ts +91 -0
- package/src/app-pwa/transport.ts +22 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/capabilities/server-invoke.ts +2 -11
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +5 -2
- package/src/cli/apps.ts +89 -26
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +26 -2
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +21 -5
- package/src/contracts/capabilities.ts +143 -3
- package/src/contracts/index.ts +2 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/pwa.ts +17 -1
- package/src/contracts/registry.ts +6 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +6 -1
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +1 -1
- package/src/server/api/respond.ts +17 -4
- package/src/server/index.ts +2 -0
- package/src/server/middleware/app-access.ts +1 -1
- package/src/server/middleware/auth.ts +21 -7
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +2 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +31 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +36 -13
- package/src/services/accounts/entities.ts +43 -19
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +19 -14
- package/src/services/accounts/identities.ts +70 -11
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +69 -4
- package/src/services/accounts/users.ts +45 -14
- package/src/services/app-access.ts +25 -7
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/ledger.ts +28 -10
- package/src/services/geo/service.ts +43 -24
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/invocation-authority.ts +4 -0
- package/src/services/identity/invocation-token.ts +238 -56
- package/src/services/identity/issue-invocation.ts +6 -1
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +13 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +20 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/session/index.ts +26 -3
- package/src/services/settings/core-settings.ts +71 -43
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/email-html.ts +1 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +433 -65
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/text-diff.ts +75 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +9 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +2 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +44 -7
- package/src/ssr/Layout.tsx +9 -7
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/PwaLayout.tsx +16 -1
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -31
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +11 -1
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/pwa-messages.ts +2 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +3 -1
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +134 -34
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { AppWorkspace, StatusBadge } from "@k2b/ui";
|
|
2
|
+
import type { JSX } from "solid-js";
|
|
3
|
+
import AdminLayout from "./AdminLayout";
|
|
4
|
+
import SettingsSaveStatus from "./SettingsSaveStatus.island";
|
|
5
|
+
|
|
6
|
+
/** One row of an area's first column. `mark` shows a dot when the section needs attention. */
|
|
7
|
+
export type AdminAreaItem = {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
href: string;
|
|
11
|
+
icon: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
mark?: "warning" | "error";
|
|
14
|
+
};
|
|
15
|
+
export type AdminAreaGroup = { title?: string; items: AdminAreaItem[] };
|
|
16
|
+
|
|
17
|
+
type AdminAreaProps = {
|
|
18
|
+
c: Parameters<typeof AdminLayout>[0]["c"];
|
|
19
|
+
/** The area's name, the head of the first column. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** Accessible name of the first column's navigation, for example "Observability sections". */
|
|
22
|
+
sectionsLabel: string;
|
|
23
|
+
/** The area's start; the first column's head links there. */
|
|
24
|
+
href: string;
|
|
25
|
+
groups: AdminAreaGroup[];
|
|
26
|
+
/** Id of the open section's item. */
|
|
27
|
+
current: string;
|
|
28
|
+
/** Page title, usually the open section's label. */
|
|
29
|
+
title: string;
|
|
30
|
+
/** Spoken with a mark in the first column, for example "Needs attention". */
|
|
31
|
+
markLabel?: string;
|
|
32
|
+
/** Id of the deepest open level: "section", or a deeper pane id the page adds. */
|
|
33
|
+
level?: string;
|
|
34
|
+
/** The section's panes, starting with one whose id is "section". */
|
|
35
|
+
children: JSX.Element;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An administration area: the admin layout, the area's sections as the first column, and the open section beside
|
|
40
|
+
* it as `AppWorkspace` levels. The owner builds each section's content by hand; a wide list adds `minSize`, so it
|
|
41
|
+
* pushes the first column aside instead of being squeezed.
|
|
42
|
+
*/
|
|
43
|
+
export function AdminArea(props: AdminAreaProps) {
|
|
44
|
+
return (
|
|
45
|
+
<AdminLayout c={props.c} title={props.title} ownMain>
|
|
46
|
+
<AppWorkspace.Main level={props.level ?? "section"}>
|
|
47
|
+
<AppWorkspace.MainPane id="area" label={props.label} href={props.href} defaultSize={248} minSize={232} maxSize={320}>
|
|
48
|
+
<nav aria-label={props.sectionsLabel}>
|
|
49
|
+
{props.groups.map((group) => (
|
|
50
|
+
<AppWorkspace.SidebarSection title={group.title}>
|
|
51
|
+
{group.items.map((item) => (
|
|
52
|
+
<AppWorkspace.SidebarItem
|
|
53
|
+
href={item.href}
|
|
54
|
+
navigation="document"
|
|
55
|
+
active={props.current === item.id}
|
|
56
|
+
icon={item.icon}
|
|
57
|
+
description={item.description}
|
|
58
|
+
meta={
|
|
59
|
+
item.mark ? (
|
|
60
|
+
<span title={props.markLabel}>
|
|
61
|
+
<StatusBadge tone={item.mark} variant="dot" label="" />
|
|
62
|
+
<span class="sr-only">{props.markLabel}</span>
|
|
63
|
+
</span>
|
|
64
|
+
) : undefined
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
{item.label}
|
|
68
|
+
</AppWorkspace.SidebarItem>
|
|
69
|
+
))}
|
|
70
|
+
</AppWorkspace.SidebarSection>
|
|
71
|
+
))}
|
|
72
|
+
</nav>
|
|
73
|
+
</AppWorkspace.MainPane>
|
|
74
|
+
{props.children}
|
|
75
|
+
</AppWorkspace.Main>
|
|
76
|
+
</AdminLayout>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The save status in a level head. A phone has no room there; `AdminAreaSaveStatusFoot` shows it at the bottom. */
|
|
81
|
+
export const AdminAreaSaveStatusHead = () => (
|
|
82
|
+
<div class="hidden md:block">
|
|
83
|
+
<SettingsSaveStatus />
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
export const AdminAreaSaveStatusFoot = () => (
|
|
87
|
+
<div class="sticky bottom-0 flex justify-end bg-(--k2b-surface) px-4 py-3 md:hidden">
|
|
88
|
+
<SettingsSaveStatus />
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
/** A section that fills the width beside the first column, such as a dashboard or one form. */
|
|
93
|
+
export function AdminAreaPane(props: {
|
|
94
|
+
label: string;
|
|
95
|
+
description?: JSX.Element;
|
|
96
|
+
/** Before the title, such as a step back inside the section. */
|
|
97
|
+
lead?: JSX.Element;
|
|
98
|
+
actions?: JSX.Element;
|
|
99
|
+
/** A form that saves with a button: its status, Discard and Save sit in the head, on a phone at the bottom. */
|
|
100
|
+
saveStatus?: boolean;
|
|
101
|
+
/** With a detail level beside it: the address of this level without the selection. */
|
|
102
|
+
href?: string;
|
|
103
|
+
/** A wide list declares the room it needs; the first column then gives way before the list is squeezed. */
|
|
104
|
+
minSize?: number;
|
|
105
|
+
children: JSX.Element;
|
|
106
|
+
}) {
|
|
107
|
+
return (
|
|
108
|
+
<AppWorkspace.MainPane
|
|
109
|
+
id="section"
|
|
110
|
+
label={props.label}
|
|
111
|
+
description={props.description}
|
|
112
|
+
lead={props.lead}
|
|
113
|
+
actions={
|
|
114
|
+
props.saveStatus ? (
|
|
115
|
+
<div class="flex items-center gap-2">
|
|
116
|
+
{props.actions}
|
|
117
|
+
<AdminAreaSaveStatusHead />
|
|
118
|
+
</div>
|
|
119
|
+
) : (
|
|
120
|
+
props.actions
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
href={props.href}
|
|
124
|
+
minSize={props.minSize}
|
|
125
|
+
defaultSize={props.minSize}
|
|
126
|
+
>
|
|
127
|
+
<div class="app-rows px-[var(--ui-space-shell)] pb-[var(--ui-space-shell)]">{props.children}</div>
|
|
128
|
+
{props.saveStatus ? <AdminAreaSaveStatusFoot /> : null}
|
|
129
|
+
</AppWorkspace.MainPane>
|
|
130
|
+
);
|
|
131
|
+
}
|
package/src/ssr/AdminLayout.tsx
CHANGED
|
@@ -30,6 +30,8 @@ type Props = {
|
|
|
30
30
|
scroll?: boolean;
|
|
31
31
|
/** Page-owned insets when its layout needs more room than the default shell spacing. */
|
|
32
32
|
contentClass?: string;
|
|
33
|
+
/** The page renders its own `AppWorkspace.Main`, such as one in level mode. */
|
|
34
|
+
ownMain?: boolean;
|
|
33
35
|
};
|
|
34
36
|
export default function AdminLayout(props: Props) {
|
|
35
37
|
// Keep children lazy so Layout installs the request locale before rendering them.
|
|
@@ -48,9 +50,13 @@ export default function AdminLayout(props: Props) {
|
|
|
48
50
|
<AppWorkspace mobileSurface="flush" class="min-h-0 flex-1" resizable={false}>
|
|
49
51
|
<AdminSidebar currentPath={currentPath} apps={runtime.apps} />
|
|
50
52
|
<AppWorkspace.Content>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
{props.ownMain ? (
|
|
54
|
+
props.children
|
|
55
|
+
) : (
|
|
56
|
+
<AppWorkspace.Main scroll={scroll} class={props.contentClass ?? (scroll ? "p-[var(--ui-space-shell)]" : undefined)}>
|
|
57
|
+
{props.children}
|
|
58
|
+
</AppWorkspace.Main>
|
|
59
|
+
)}
|
|
54
60
|
</AppWorkspace.Content>
|
|
55
61
|
</AppWorkspace>
|
|
56
62
|
</Layout>
|
package/src/ssr/AdminSidebar.tsx
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { AppWorkspace, useLocale } from "@k2b/ui";
|
|
2
|
-
import {
|
|
2
|
+
import { activeAdminLink } from "./admin-active-link";
|
|
3
3
|
import { type AdminLink, buildAdminGroups } from "./admin-navigation";
|
|
4
4
|
import { platformMessages } from "./platform-messages";
|
|
5
5
|
import type { RuntimeContext } from "./runtime";
|
|
6
6
|
import WorkspaceNavigation from "./WorkspaceNavigation.island";
|
|
7
7
|
|
|
8
8
|
const AdminNavigation = (props: { currentPath: string; groups: ReturnType<typeof buildAdminGroups> }) => {
|
|
9
|
-
const activeHref =
|
|
9
|
+
const activeHref = activeAdminLink(
|
|
10
10
|
props.currentPath,
|
|
11
|
-
props.groups.flatMap((group) => group.links
|
|
12
|
-
);
|
|
11
|
+
props.groups.flatMap((group) => group.links),
|
|
12
|
+
)?.href;
|
|
13
13
|
return (
|
|
14
14
|
<>
|
|
15
15
|
{props.groups.map((group) => (
|
|
@@ -45,10 +45,10 @@ export default function AdminSidebar({ currentPath, apps }: { currentPath: strin
|
|
|
45
45
|
icon: `ti ${link.icon}`,
|
|
46
46
|
active:
|
|
47
47
|
link.href ===
|
|
48
|
-
|
|
48
|
+
activeAdminLink(
|
|
49
49
|
currentPath,
|
|
50
|
-
groups().flatMap((entry) => entry.links
|
|
51
|
-
),
|
|
50
|
+
groups().flatMap((entry) => entry.links),
|
|
51
|
+
)?.href,
|
|
52
52
|
})),
|
|
53
53
|
}))}
|
|
54
54
|
/>
|
|
@@ -40,6 +40,8 @@ export function AppAccessDeniedContent(props: {
|
|
|
40
40
|
<p class="mb-4 text-[15px] text-secondary">
|
|
41
41
|
{props.revoked ? (
|
|
42
42
|
t().revokedDescription
|
|
43
|
+
) : props.app.access?.kind === "resources" ? (
|
|
44
|
+
t().deniedResources
|
|
43
45
|
) : grants() ? (
|
|
44
46
|
t().deniedGrants
|
|
45
47
|
) : (
|
package/src/ssr/AppBadge.tsx
CHANGED
|
@@ -18,6 +18,20 @@ export const useAppBadgeDescription = (input: () => { endpoint?: string; appId?:
|
|
|
18
18
|
* takes layout space, so appearing, growing to "99+", or clearing moves
|
|
19
19
|
* nothing. Decorative: the link carries the description.
|
|
20
20
|
*/
|
|
21
|
+
/**
|
|
22
|
+
* The count on a phone bar item: the rail's count, laid over the tab icon's corner, and its description for screen
|
|
23
|
+
* readers after the item's label.
|
|
24
|
+
*/
|
|
25
|
+
export function TabBarAppBadge(props: { endpoint?: string; appId?: string }) {
|
|
26
|
+
const description = useAppBadgeDescription(() => props);
|
|
27
|
+
return (
|
|
28
|
+
<Show when={description()}>
|
|
29
|
+
<AppBadge endpoint={props.endpoint} appId={props.appId} />
|
|
30
|
+
<span class="sr-only">, {description()}</span>
|
|
31
|
+
</Show>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
export function AppBadge(props: { endpoint?: string; appId?: string }) {
|
|
22
36
|
const count = () => appBadgeCount(props);
|
|
23
37
|
return (
|
|
@@ -29,7 +29,7 @@ export type AppLaunchpadLegalLink = {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
export type AppLaunchpadContext = {
|
|
32
|
-
profile?: { name: string; theme: CloudThemePreference; appHref?: string };
|
|
32
|
+
profile?: { userId?: string; name: string; theme: CloudThemePreference; appHref?: string };
|
|
33
33
|
apps: AppLaunchpadApp[];
|
|
34
34
|
legalLinks: AppLaunchpadLegalLink[];
|
|
35
35
|
};
|
|
@@ -42,7 +42,7 @@ const appIconPalette: readonly [AppIconPaletteEntry, ...AppIconPaletteEntry[]] =
|
|
|
42
42
|
{ from: "#7c3aed" },
|
|
43
43
|
{ from: "#d97706" },
|
|
44
44
|
{ from: "#e11d48" },
|
|
45
|
-
{ from: "#
|
|
45
|
+
{ from: "#c026d3" },
|
|
46
46
|
{ from: "#52525b" },
|
|
47
47
|
];
|
|
48
48
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { TabBar, useLocale } from "@k2b/ui";
|
|
2
2
|
import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from "solid-js";
|
|
3
3
|
import { isServer, Portal } from "solid-js/web";
|
|
4
|
+
import { TabBarAppBadge } from "./AppBadge";
|
|
5
|
+
import { appBadgeCount } from "./app-badges";
|
|
4
6
|
import { AppLaunchpadButton, setAppLaunchpadContext } from "./app-launchpad";
|
|
5
7
|
import GlobalSearchTrigger from "./GlobalSearchTrigger";
|
|
6
8
|
import LayoutBreadcrumbs from "./LayoutBreadcrumbs";
|
|
@@ -35,8 +37,9 @@ export default function CloudShell(props: CloudShellProps) {
|
|
|
35
37
|
/**
|
|
36
38
|
* The phone bar's live copy, mounted with the rail's island so the shell adds no island instance. It renders nothing
|
|
37
39
|
* while the server's bar is current, so mounting moves and refocuses nothing. When the saved rail preferences or the
|
|
38
|
-
* URL (a fragment, Back and Forward, an in-page navigation) change what the bar shows,
|
|
39
|
-
* bar's row, and CSS hides the server's.
|
|
40
|
+
* URL (a fragment, Back and Forward, an in-page navigation) change what the bar shows, or an app in it has a count,
|
|
41
|
+
* it renders the new bar into the bar's row, and CSS hides the server's. Counts, like the rail's, exist only in the
|
|
42
|
+
* browser.
|
|
40
43
|
*/
|
|
41
44
|
function BarNavigation(props: { currentPath: string }) {
|
|
42
45
|
if (isServer) return null;
|
|
@@ -51,6 +54,12 @@ function BarNavigation(props: { currentPath: string }) {
|
|
|
51
54
|
phoneBarItems(projectRailNavigation(data.apps, rail, locale()), path, { label: t().home, path: data.home ?? "/" });
|
|
52
55
|
const served = JSON.stringify(itemsFor(initial, props.currentPath));
|
|
53
56
|
const items = createMemo(() => itemsFor(settings(), currentPath()));
|
|
57
|
+
const counted = () => items().some((item) => item.counted && appBadgeCount(item.counted) > 0);
|
|
58
|
+
const live = createMemo(() =>
|
|
59
|
+
items().map((item) =>
|
|
60
|
+
item.counted ? { ...item, badge: <TabBarAppBadge endpoint={item.counted.endpoint} appId={item.counted.appId} /> } : item,
|
|
61
|
+
),
|
|
62
|
+
);
|
|
54
63
|
onMount(() => {
|
|
55
64
|
const sync = () => setSettings(readRailContext()?.settings ?? initial);
|
|
56
65
|
const syncLocation = () => setCurrentPath(`${window.location.pathname}${window.location.search}${window.location.hash}`);
|
|
@@ -76,10 +85,22 @@ function BarNavigation(props: { currentPath: string }) {
|
|
|
76
85
|
});
|
|
77
86
|
const row = document.querySelector<HTMLElement>(".cloud-tab-bar__row");
|
|
78
87
|
if (!row) return null;
|
|
88
|
+
// Swapping bars keeps a focused link: the bar that takes over focuses the same destination, once CSS shows it.
|
|
89
|
+
const [shown, setShown] = createSignal(false);
|
|
90
|
+
const focusIn = (selector: string, tab: string) =>
|
|
91
|
+
[...row.querySelectorAll<HTMLElement>(`${selector} a[data-tab]`)].find((link) => link.dataset.tab === tab)?.focus();
|
|
92
|
+
createEffect(() => {
|
|
93
|
+
const next = counted() || JSON.stringify(items()) !== served;
|
|
94
|
+
if (next === shown()) return;
|
|
95
|
+
const focused = document.activeElement;
|
|
96
|
+
const tab = focused instanceof HTMLElement && row.contains(focused) ? focused.dataset.tab : undefined;
|
|
97
|
+
setShown(next);
|
|
98
|
+
if (tab) queueMicrotask(() => focusIn(next ? ".cloud-tab-bar__live" : ".k2b-tab-bar:not(.cloud-tab-bar__live)", tab));
|
|
99
|
+
});
|
|
79
100
|
return (
|
|
80
|
-
<Show when={
|
|
101
|
+
<Show when={shown()}>
|
|
81
102
|
<Portal mount={row}>
|
|
82
|
-
<TabBar variant="floating" class="cloud-tab-bar__live" label={t().apps} items={
|
|
103
|
+
<TabBar variant="floating" class="cloud-tab-bar__live" label={t().apps} items={live()} />
|
|
83
104
|
</Portal>
|
|
84
105
|
</Show>
|
|
85
106
|
);
|
|
@@ -90,7 +111,12 @@ function RailNavigation(props: { currentPath: string; panelOpen?: boolean }) {
|
|
|
90
111
|
const locale = useLocale();
|
|
91
112
|
const t = () => platformMessages.resolve([locale()]).t;
|
|
92
113
|
if (!data?.rail || !data.profile) return null;
|
|
93
|
-
const profile = {
|
|
114
|
+
const profile = {
|
|
115
|
+
userId: data.profile.userId,
|
|
116
|
+
name: data.profile.name,
|
|
117
|
+
theme: data.theme,
|
|
118
|
+
...(data.profile.appHref ? { appHref: data.profile.appHref } : {}),
|
|
119
|
+
};
|
|
94
120
|
createEffect(() => setAppLaunchpadContext(data.apps, data.legalLinks, profile));
|
|
95
121
|
return (
|
|
96
122
|
<nav class="layout-rail-navigation flex min-h-0 flex-1 flex-col items-center gap-1" aria-label={t().apps}>
|
|
@@ -107,6 +133,7 @@ function RailNavigation(props: { currentPath: string; panelOpen?: boolean }) {
|
|
|
107
133
|
helpOpen={!!props.panelOpen && data.panel?.provider === "core"}
|
|
108
134
|
/>
|
|
109
135
|
<ProfilePreferences
|
|
136
|
+
userId={data.profile.userId}
|
|
110
137
|
appHref={data.profile.appHref}
|
|
111
138
|
avatarSrc={data.profile.avatarSrc}
|
|
112
139
|
initialTheme={data.theme}
|
|
@@ -143,7 +170,12 @@ function HeaderControls(props: {
|
|
|
143
170
|
<AppLaunchpadButton
|
|
144
171
|
profile={
|
|
145
172
|
data.profile
|
|
146
|
-
? {
|
|
173
|
+
? {
|
|
174
|
+
userId: data.profile.userId,
|
|
175
|
+
name: data.profile.name,
|
|
176
|
+
theme: data.theme,
|
|
177
|
+
...(data.profile.appHref ? { appHref: data.profile.appHref } : {}),
|
|
178
|
+
}
|
|
147
179
|
: undefined
|
|
148
180
|
}
|
|
149
181
|
apps={data.apps}
|
|
@@ -167,7 +199,12 @@ function HeaderControls(props: {
|
|
|
167
199
|
<AppLaunchpadButton
|
|
168
200
|
profile={
|
|
169
201
|
data.profile
|
|
170
|
-
? {
|
|
202
|
+
? {
|
|
203
|
+
userId: data.profile.userId,
|
|
204
|
+
name: data.profile.name,
|
|
205
|
+
theme: data.theme,
|
|
206
|
+
...(data.profile.appHref ? { appHref: data.profile.appHref } : {}),
|
|
207
|
+
}
|
|
171
208
|
: undefined
|
|
172
209
|
}
|
|
173
210
|
apps={data.apps}
|
package/src/ssr/Layout.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { isAppSessionActor } from "../server/actor";
|
|
|
14
14
|
import { getLocale } from "../server/locale";
|
|
15
15
|
import { getDateConfig } from "../server/time";
|
|
16
16
|
import { dates, resolveHomePath } from "../shared";
|
|
17
|
+
import { publicCloudOrigin } from "../shared/app-url";
|
|
17
18
|
import { readThemePreferenceFromCookieHeader } from "../shared/theme";
|
|
18
19
|
import AnnouncementBanners from "./AnnouncementBanners";
|
|
19
20
|
import { appAppearanceStyle, resolveCurrentApp } from "./app-appearance";
|
|
@@ -258,7 +259,9 @@ export default function Layout(props: LayoutProps) {
|
|
|
258
259
|
const panel: ShellData["panel"] = providers.length
|
|
259
260
|
? { providers, provider: panelState.provider ?? providers[0]?.id, context: pageContext, consent: c.get("panelConsent") }
|
|
260
261
|
: undefined;
|
|
262
|
+
const cloudOrigin = settings?.app?.url ? publicCloudOrigin(settings.app.url) : undefined;
|
|
261
263
|
const shellData: ShellData = {
|
|
264
|
+
...(cloudOrigin ? { cloudOrigin } : {}),
|
|
262
265
|
...(panel ? { panel } : {}),
|
|
263
266
|
platform: keyboardPlatform(c.req.raw.headers.get("User-Agent") ?? ""),
|
|
264
267
|
...(viewHelp.length ? { help: viewHelp } : {}),
|
|
@@ -268,7 +271,7 @@ export default function Layout(props: LayoutProps) {
|
|
|
268
271
|
? {
|
|
269
272
|
rail: c.get("railPreferences") ?? defaultRailPreferences(),
|
|
270
273
|
home: resolveHomePath(settings?.app?.home_path),
|
|
271
|
-
profile: { name: profileName, avatarSrc: profileAvatarSrc, appHref },
|
|
274
|
+
profile: { userId: user.id, name: profileName, avatarSrc: profileAvatarSrc, appHref },
|
|
272
275
|
}
|
|
273
276
|
: {}),
|
|
274
277
|
legalLinks,
|
|
@@ -294,6 +297,7 @@ export default function Layout(props: LayoutProps) {
|
|
|
294
297
|
apps: allApps,
|
|
295
298
|
legalLinks,
|
|
296
299
|
searchLinks,
|
|
300
|
+
cloudOrigin: shellData.cloudOrigin,
|
|
297
301
|
home: shellData.home,
|
|
298
302
|
profile: shellData.profile,
|
|
299
303
|
announcements: user ? (announcements?.latestAnnouncementVersion ?? null) : null,
|
|
@@ -325,12 +329,10 @@ export default function Layout(props: LayoutProps) {
|
|
|
325
329
|
<AppWorkspace.LayoutStateProvider state={workspaceLayout}>{props.children}</AppWorkspace.LayoutStateProvider>
|
|
326
330
|
</main>
|
|
327
331
|
</PageTrailProvider>
|
|
328
|
-
{/* Focus pages
|
|
329
|
-
{panel
|
|
330
|
-
<
|
|
331
|
-
|
|
332
|
-
</ShellDataProvider>
|
|
333
|
-
)}
|
|
332
|
+
{/* Focus pages also expose the canonical origin to their islands. */}
|
|
333
|
+
<ShellDataProvider data={{ cloudOrigin, panel, platform: shellData.platform, theme, apps: [], legalLinks: [], searchLinks: {} }}>
|
|
334
|
+
{panel && <PanelSlot panel={panel} state={{ ...panelState, open: false }} platform={shellData.platform} />}
|
|
335
|
+
</ShellDataProvider>
|
|
334
336
|
<div data-print-fallback="">
|
|
335
337
|
<p>{t.printFallbackTitle}</p>
|
|
336
338
|
<p>{props.printHint ?? t.printFallbackHint}</p>
|
|
@@ -5,10 +5,10 @@ import { createProfileActions } from "./profile-actions";
|
|
|
5
5
|
import { ThemeCycleButton } from "./ThemeCycleButton";
|
|
6
6
|
|
|
7
7
|
export function MobileProfileActions(props: {
|
|
8
|
-
profile: { name: string; theme: CloudThemePreference; appHref?: string };
|
|
8
|
+
profile: { userId?: string; name: string; theme: CloudThemePreference; appHref?: string };
|
|
9
9
|
beforeSelect: () => Promise<boolean>;
|
|
10
10
|
}) {
|
|
11
|
-
const { preferences, signingOut, signOut } = createProfileActions(props.profile.theme, props.profile.appHref);
|
|
11
|
+
const { preferences, signingOut, signOut } = createProfileActions(props.profile.theme, props.profile.appHref, props.profile.userId);
|
|
12
12
|
const navigate = (href: string) => (event: MouseEvent) => {
|
|
13
13
|
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
14
14
|
event.preventDefault();
|
|
@@ -3,6 +3,7 @@ import type { CloudThemePreference } from "../shared/theme";
|
|
|
3
3
|
import { createProfileActions } from "./profile-actions";
|
|
4
4
|
|
|
5
5
|
type ProfilePreferencesProps = {
|
|
6
|
+
userId?: string;
|
|
6
7
|
/** Set while the installation offers the mobile app. */
|
|
7
8
|
appHref?: string;
|
|
8
9
|
avatarSrc?: string;
|
|
@@ -16,7 +17,7 @@ type ProfilePreferencesProps = {
|
|
|
16
17
|
* bottom edge.
|
|
17
18
|
*/
|
|
18
19
|
export default function ProfilePreferences(props: ProfilePreferencesProps) {
|
|
19
|
-
const { preferences, items, signOut, signingOut } = createProfileActions(props.initialTheme, props.appHref);
|
|
20
|
+
const { preferences, items, signOut, signingOut } = createProfileActions(props.initialTheme, props.appHref, props.userId);
|
|
20
21
|
|
|
21
22
|
const avatar = () => <Avatar name={props.name} src={props.avatarSrc} size="sm" />;
|
|
22
23
|
|
package/src/ssr/PwaLayout.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { pwaBuild } from "../contracts/pwa-builds";
|
|
|
7
7
|
import { getLocale } from "../server/locale";
|
|
8
8
|
import { visiblePwaParts } from "./app-navigation";
|
|
9
9
|
import type { LayoutContext } from "./layout-context";
|
|
10
|
+
import PwaPartCount from "./PwaPartCount.island";
|
|
10
11
|
import PwaRuntime from "./PwaRuntime.island";
|
|
11
12
|
import { pwaMessages } from "./pwa-messages";
|
|
12
13
|
import { getDisplayRuntimeContext } from "./runtime";
|
|
@@ -62,12 +63,24 @@ export default function PwaLayout(props: PwaLayoutProps) {
|
|
|
62
63
|
const ownBuild = pwaBuild(buildMetadata);
|
|
63
64
|
const coreBuild = registeredPwaBuilds().core;
|
|
64
65
|
const builds = { ...(owner && ownBuild ? { [owner]: ownBuild } : {}), ...(coreBuild ? { core: coreBuild } : {}) };
|
|
66
|
+
// A part's badge calls for attention in the sidebar and on its tab; without one, its summary's count stays quiet in
|
|
67
|
+
// the sidebar. One watcher reads every route of the page.
|
|
68
|
+
const countRoutes = [...new Set(parts.flatMap((part) => [part.pwa.badge, part.pwa.summary].filter((path) => path !== undefined)))];
|
|
69
|
+
const badge = (part: (typeof parts)[number]) =>
|
|
70
|
+
part.pwa.badge ? <PwaPartCount endpoint={part.pwa.badge} endpoints={countRoutes} attention /> : undefined;
|
|
65
71
|
const tabs = parts.slice(0, TAB_PARTS);
|
|
66
72
|
const currentTab = tabs.find((part) => path === part.pwa.href || path.startsWith(`${part.pwa.href}/`));
|
|
67
73
|
// Start is titled with the installation's name; a part's first page carries the part's name.
|
|
68
74
|
const items: TabBarItem[] = [
|
|
69
75
|
{ id: "start", label: t.start, title: cloud, icon: "ti ti-home", href: PWA_SCOPE, current: !currentTab },
|
|
70
|
-
...tabs.map((part) => ({
|
|
76
|
+
...tabs.map((part) => ({
|
|
77
|
+
id: part.id,
|
|
78
|
+
label: part.name,
|
|
79
|
+
icon: part.icon,
|
|
80
|
+
href: part.pwa.href,
|
|
81
|
+
current: part === currentTab,
|
|
82
|
+
badge: badge(part),
|
|
83
|
+
})),
|
|
71
84
|
];
|
|
72
85
|
// The sidebar names the installation itself, so Start is titled Start there; Settings is a destination of its own.
|
|
73
86
|
const settingsHref = `${PWA_SCOPE}settings`;
|
|
@@ -87,6 +100,8 @@ export default function PwaLayout(props: PwaLayoutProps) {
|
|
|
87
100
|
icon: part.icon,
|
|
88
101
|
href: part.pwa.href,
|
|
89
102
|
current: part === currentPart,
|
|
103
|
+
badge: badge(part),
|
|
104
|
+
count: part.pwa.summary ? <PwaPartCount endpoint={part.pwa.summary} endpoints={countRoutes} /> : undefined,
|
|
90
105
|
})),
|
|
91
106
|
},
|
|
92
107
|
]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AppWorkspace } from "@k2b/ui";
|
|
2
|
+
import { onCleanup, onMount, Show } from "solid-js";
|
|
3
|
+
import { appBadgeCount, watchAppBadges } from "./app-badges";
|
|
4
|
+
|
|
5
|
+
export type PwaPartCountProps = {
|
|
6
|
+
/** This part's `pwa.badge` or, without one, its `pwa.summary` route. */
|
|
7
|
+
endpoint: string;
|
|
8
|
+
/** Every count route of the page; the first count on a page reads them all, once a minute like the web's. */
|
|
9
|
+
endpoints: string[];
|
|
10
|
+
/** A badge calls for attention, such as unread messages; a summary's count stays quiet beside the label. */
|
|
11
|
+
attention?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** The count of a part in the Cloud app's sidebar and tab bar; nothing at zero. */
|
|
15
|
+
export default function PwaPartCount(props: PwaPartCountProps) {
|
|
16
|
+
onMount(() => onCleanup(watchAppBadges(props.endpoints)));
|
|
17
|
+
const count = () => appBadgeCount({ endpoint: props.endpoint });
|
|
18
|
+
return (
|
|
19
|
+
<Show when={props.attention} fallback={<Show when={count() > 0}>{count() > 99 ? "99+" : count()}</Show>}>
|
|
20
|
+
<AppWorkspace.SidebarItemStatus unread={count()} />
|
|
21
|
+
</Show>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
import { Button, InlineGuidance, useLocale } from "@k2b/ui";
|
|
3
|
+
import { Show } from "solid-js";
|
|
4
|
+
import type { SettingsDraftOutcome } from "./settings-draft";
|
|
5
|
+
|
|
6
|
+
const messages = i18n.define({
|
|
7
|
+
baseLocale: "en",
|
|
8
|
+
messages: {
|
|
9
|
+
en: {
|
|
10
|
+
notSaved: "Not saved.",
|
|
11
|
+
notSavedKeep: "The previous values still apply. Your changes stay here.",
|
|
12
|
+
unknownOutcome: "The connection broke while saving.",
|
|
13
|
+
unknownOutcomeKeep: "Whether the new values apply is open. Cloud does not save again on its own.",
|
|
14
|
+
checkNow: "Check current state",
|
|
15
|
+
retry: "Try again",
|
|
16
|
+
},
|
|
17
|
+
de: {
|
|
18
|
+
notSaved: "Nicht gespeichert.",
|
|
19
|
+
notSavedKeep: "Es gelten weiter die alten Werte. Deine Änderungen bleiben hier stehen.",
|
|
20
|
+
unknownOutcome: "Die Verbindung brach beim Speichern ab.",
|
|
21
|
+
unknownOutcomeKeep: "Ob die neuen Werte gelten, ist offen. Cloud speichert nicht von selbst noch einmal.",
|
|
22
|
+
checkNow: "Stand jetzt prüfen",
|
|
23
|
+
retry: "Erneut versuchen",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const checkSettingsSaveOutcomeMessages = () => messages.check();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What became of the last save when it did not succeed, above the form: not saved with the reason and a retry, or an
|
|
32
|
+
* unclear outcome with a way to check the current state instead of saving twice.
|
|
33
|
+
*/
|
|
34
|
+
export default function SettingsSaveOutcome(props: { outcome: SettingsDraftOutcome | null; onRetry: () => void }) {
|
|
35
|
+
const locale = useLocale();
|
|
36
|
+
const t = () => messages.resolve([locale()]).t;
|
|
37
|
+
return (
|
|
38
|
+
<Show when={props.outcome}>
|
|
39
|
+
{(outcome) =>
|
|
40
|
+
outcome().kind === "failed" ? (
|
|
41
|
+
<InlineGuidance tone="danger" icon="ti ti-alert-triangle">
|
|
42
|
+
<b>{t().notSaved}</b> {outcome().message} {t().notSavedKeep}{" "}
|
|
43
|
+
<Button type="button" variant="ghost" size="xs" onClick={props.onRetry}>
|
|
44
|
+
{t().retry}
|
|
45
|
+
</Button>
|
|
46
|
+
</InlineGuidance>
|
|
47
|
+
) : (
|
|
48
|
+
<InlineGuidance tone="warning" icon="ti ti-help-circle">
|
|
49
|
+
<b>{t().unknownOutcome}</b> {t().unknownOutcomeKeep}{" "}
|
|
50
|
+
<Button type="button" variant="ghost" size="xs" onClick={() => window.location.reload()}>
|
|
51
|
+
{t().checkNow}
|
|
52
|
+
</Button>
|
|
53
|
+
</InlineGuidance>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
</Show>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SaveStatus } from "@k2b/ui";
|
|
2
|
+
import { settingsSaveControls } from "./settings-draft";
|
|
3
|
+
|
|
4
|
+
/** Status, Discard and Save in the head of a form level. Until the form registers, it shows "Saved" with both buttons disabled. */
|
|
5
|
+
export default function SettingsSaveStatus() {
|
|
6
|
+
const controls = settingsSaveControls;
|
|
7
|
+
return (
|
|
8
|
+
<SaveStatus
|
|
9
|
+
state={() => controls()?.state ?? "saved"}
|
|
10
|
+
savedAt={() => controls()?.savedAt}
|
|
11
|
+
invalidCount={() => controls()?.invalidCount ?? 1}
|
|
12
|
+
onSave={() => controls()?.save()}
|
|
13
|
+
onDiscard={() => controls()?.discard()}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|