@k2b/cloud 0.38.0 → 0.40.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/README.md +8 -0
- package/package.json +12 -5
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/app-path.ts +25 -0
- package/src/_internal/app-presentation.ts +4 -0
- package/src/_internal/canvas-worker.ts +5 -3
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +63 -16
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/panel-cookie.ts +39 -0
- package/src/_internal/registry-validation.ts +21 -26
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +32 -7
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/background-review.ts +72 -0
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +398 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +669 -0
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +45 -163
- package/src/ai/chat/view-block.tsx +22 -7
- package/src/ai/chat-task-contracts.ts +31 -3
- package/src/ai/chat-task-reasons-migrate.ts +75 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +438 -78
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +14 -0
- package/src/ai/migrate.ts +39 -4
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +172 -2
- package/src/ai/runtime.ts +13 -27
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +232 -27
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +67 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +40 -4
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-core-settings.ts +190 -139
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +6 -2
- package/src/api/app-approval.ts +54 -6
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +3 -1
- package/src/api/auth.ts +25 -6
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +7 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/app-approval.ts +60 -6
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +145 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +160 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +35 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +756 -0
- package/src/browser/panel.ts +65 -0
- package/src/browser/resource-picker.tsx +12 -1
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +159 -12
- package/src/contracts/app.ts +14 -0
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +2 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/server/services/access.ts +5 -1
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/app-approval.ts +533 -90
- package/src/services/approvals/device.ts +9 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +32 -3
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/browser.ts +16 -5
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/pdf/image-protocol.ts +135 -0
- package/src/services/pdf/image-variants.ts +124 -0
- package/src/services/pdf/image-worker.ts +31 -0
- package/src/services/pdf/image.ts +109 -0
- package/src/services/pdf/index.ts +9 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/settings/api.ts +11 -6
- package/src/services/settings/app.ts +5 -4
- package/src/services/settings/core-settings.ts +4 -3
- package/src/services/settings/index.ts +19 -17
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/services/webauthn.ts +12 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared/redirect.ts +12 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +95 -10
- package/src/ssr/Layout.tsx +66 -10
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +35 -0
- package/src/ssr/PanelRuntime.island.tsx +94 -0
- package/src/ssr/PanelSlot.tsx +158 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +69 -5
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +105 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +28 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +439 -0
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +159 -0
- package/src/ssr/platform-messages.ts +48 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +5 -0
- package/src/ssr/shell-shortcuts.ts +13 -2
- package/src/styles/effects.css +201 -11
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +314 -63
- package/src/styles/panel.css +416 -0
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +36 -13
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +22 -43
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import { dates } from "@k2b/stdlib";
|
|
2
|
-
import {
|
|
3
|
-
Button,
|
|
4
|
-
ButtonLink,
|
|
5
|
-
type DropdownItem,
|
|
6
|
-
dialogCore,
|
|
7
|
-
MarkdownView,
|
|
8
|
-
PanelDialog,
|
|
9
|
-
Placeholder,
|
|
10
|
-
panelDialogOptions,
|
|
11
|
-
prompts,
|
|
12
|
-
SplitButton,
|
|
13
|
-
useLocale,
|
|
14
|
-
} from "@k2b/ui";
|
|
2
|
+
import { Button, dialogCore, PanelDialog, Placeholder, panelDialogOptions, prompts, useLocale } from "@k2b/ui";
|
|
15
3
|
import { createMemo, createSignal, For, type JSX, onCleanup, onMount, Show } from "solid-js";
|
|
4
|
+
import { capabilityQuestionText, capabilityUnknownText } from "../_internal/capability-sentences";
|
|
5
|
+
import { isLegacyActionReview, storedActionReview } from "../ai/action-review";
|
|
6
|
+
import { type ActionCard, legacyActionCard, reviewActionCard } from "../ai/chat/action-card";
|
|
7
|
+
import { actionMessages } from "../ai/chat/action-messages";
|
|
8
|
+
import { ActionQuestion, ActionReceipt } from "../ai/chat/action-question";
|
|
9
|
+
import { aiChatMessages } from "../ai/chat/messages";
|
|
16
10
|
import { refreshAppBadges } from "../browser/app-badges";
|
|
17
11
|
import { coreClient } from "../clients/core";
|
|
18
12
|
import {
|
|
@@ -23,13 +17,10 @@ import {
|
|
|
23
17
|
ApprovalPageSchema,
|
|
24
18
|
type PendingApproval,
|
|
25
19
|
} from "../contracts/approvals";
|
|
26
|
-
import type { CapabilityActionReview } from "../contracts/capabilities";
|
|
27
20
|
import { approvalMessages } from "./messages";
|
|
28
21
|
|
|
29
22
|
/** How a decided request ends in this list. It stays in place as a receipt until the next load. */
|
|
30
23
|
type Outcome = "approved" | "rejected" | "elsewhere" | "gone";
|
|
31
|
-
/** Approved and rejected are this person's decision; the other two mean the click changed nothing. */
|
|
32
|
-
const decidedHere = (outcome: Outcome) => outcome === "approved" || outcome === "rejected";
|
|
33
24
|
/** One icon per outcome, so a rejection or a decision made elsewhere never looks like a success. */
|
|
34
25
|
const outcomeIcon = (outcome: Outcome, band = false) =>
|
|
35
26
|
outcome === "approved"
|
|
@@ -41,8 +32,9 @@ const outcomeIcon = (outcome: Outcome, band = false) =>
|
|
|
41
32
|
? "ti ti-circle-x"
|
|
42
33
|
: "ti ti-x"
|
|
43
34
|
: "ti ti-info-circle";
|
|
44
|
-
type ReviewDetail = NonNullable<CapabilityActionReview["details"]>[number];
|
|
45
35
|
|
|
36
|
+
/** Keep SSR and the first client render identical: the page has no reader zone of its own yet. */
|
|
37
|
+
const READER_TIME_ZONE = "UTC";
|
|
46
38
|
export type ApprovalsProps = {
|
|
47
39
|
/** List the requests whose effect belongs to this app or that this app asked for. Without, every app's, grouped. */
|
|
48
40
|
app?: string;
|
|
@@ -51,8 +43,8 @@ export type ApprovalsProps = {
|
|
|
51
43
|
/** The first page for the same query, loaded during SSR with `loadPendingApprovals`; without, the browser loads it. */
|
|
52
44
|
initial?: ApprovalPage;
|
|
53
45
|
/**
|
|
54
|
-
* `list` shows the
|
|
55
|
-
* row above the resource it would change, and nothing when none waits.
|
|
46
|
+
* `list` shows each request as the same question card as the chat, with a count and an empty state. `band` shows
|
|
47
|
+
* each request as one compact row above the resource it would change, and nothing when none waits.
|
|
56
48
|
*/
|
|
57
49
|
layout?: "list" | "band";
|
|
58
50
|
};
|
|
@@ -79,66 +71,48 @@ const approvalTitle = (item: PendingApproval): string => {
|
|
|
79
71
|
};
|
|
80
72
|
|
|
81
73
|
/** Without a review, the sentence's later `Label: value` lines are what the person approves. */
|
|
82
|
-
const
|
|
74
|
+
const sentenceRows = (item: PendingApproval) =>
|
|
83
75
|
lines(item)
|
|
84
76
|
.slice(1)
|
|
85
77
|
.map((line) => {
|
|
86
78
|
const match = /^([^:]{1,40}):\s+(.+)$/.exec(line);
|
|
87
|
-
return match ? { label: match[1]
|
|
79
|
+
return match ? { label: match[1]!, value: match[2] ?? "" } : { label: "", value: line };
|
|
88
80
|
});
|
|
89
81
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
</Show>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** The review's message, or the sentence's plain lines when the Action has no review. */
|
|
125
|
-
function Description(props: { item: PendingApproval }) {
|
|
126
|
-
const text = () => {
|
|
127
|
-
const message = props.item.review?.message.trim();
|
|
128
|
-
if (message) return message === approvalTitle(props.item) ? null : message;
|
|
129
|
-
const plain = sentenceFields(props.item)
|
|
130
|
-
.filter((field) => field.label === null)
|
|
131
|
-
.map((field) => field.value);
|
|
132
|
-
return plain.length ? plain.join("\n") : null;
|
|
133
|
-
};
|
|
134
|
-
return (
|
|
135
|
-
<Show when={text()}>
|
|
136
|
-
<p class="cloud-approval__text">{text()}</p>
|
|
137
|
-
</Show>
|
|
82
|
+
/**
|
|
83
|
+
* The same card as the chat: a new review gives the app's sentence, reason, and preview; an old review its literal
|
|
84
|
+
* message and rows; a request without a review its sentence. Times show in the reader's zone (UTC until the page
|
|
85
|
+
* passes one, the same on the server and in the first browser render).
|
|
86
|
+
*/
|
|
87
|
+
const approvalCard = (item: PendingApproval, locale: string, timeZone: string): ActionCard => {
|
|
88
|
+
const app = { name: item.app.name, icon: item.app.icon ?? "ti ti-shield-check" };
|
|
89
|
+
const context = { locale, timeZone, app };
|
|
90
|
+
const review = item.review ? storedActionReview(item.review) : null;
|
|
91
|
+
if (review && !isLegacyActionReview(review)) return reviewActionCard(review, context);
|
|
92
|
+
const title = approvalTitle(item);
|
|
93
|
+
const t = aiChatMessages(locale);
|
|
94
|
+
return legacyActionCard(
|
|
95
|
+
{
|
|
96
|
+
question: review?.legacy.question ?? capabilityQuestionText(title, locale),
|
|
97
|
+
rows: review?.legacy.rows ?? sentenceRows(item),
|
|
98
|
+
...(review?.legacy.markdown ? { markdown: review.legacy.markdown } : {}),
|
|
99
|
+
...(review?.legacy.links ? { links: review.legacy.links } : {}),
|
|
100
|
+
subject: title,
|
|
101
|
+
title,
|
|
102
|
+
outcome: {
|
|
103
|
+
running: t.receiptRunning({ title }),
|
|
104
|
+
done: t.receiptDone({ title }),
|
|
105
|
+
rejected: t.receiptRejected({ title }),
|
|
106
|
+
notRun: t.receiptNotRun({ title, stopped: false }),
|
|
107
|
+
unknown: capabilityUnknownText(title, locale),
|
|
108
|
+
unknownStill: capabilityUnknownText(title, locale, true),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
context,
|
|
138
112
|
);
|
|
139
|
-
}
|
|
113
|
+
};
|
|
140
114
|
|
|
141
|
-
/** Origin of the request: owning app, the chat it waits in,
|
|
115
|
+
/** Origin of the request: owning app, the chat it waits in, when Cloud asked, and when it expires. */
|
|
142
116
|
function Origin(props: { item: PendingApproval; withApp?: boolean }) {
|
|
143
117
|
const t = useMessages();
|
|
144
118
|
const locale = useLocale();
|
|
@@ -153,168 +127,131 @@ function Origin(props: { item: PendingApproval; withApp?: boolean }) {
|
|
|
153
127
|
</a>
|
|
154
128
|
<span aria-hidden="true"> · </span>
|
|
155
129
|
<time dateTime={props.item.createdAt}>{dates.formatDateTimeRelative(props.item.createdAt, { locale: locale() })}</time>
|
|
130
|
+
<span aria-hidden="true"> · </span>
|
|
131
|
+
<span>{t().expiresIn({ time: dates.formatTimeSpan(props.item.expiresAt, { locale: locale() }) })}</span>
|
|
156
132
|
</p>
|
|
157
133
|
);
|
|
158
134
|
}
|
|
159
135
|
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const text
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
role={props.failed ? "alert" : undefined}
|
|
185
|
-
>
|
|
186
|
-
<i class={icon()} aria-hidden="true" />
|
|
187
|
-
<span title={text()}>{text()}</span>
|
|
188
|
-
</span>
|
|
189
|
-
);
|
|
136
|
+
/** The outcome lines a request on this page can reach; the chat it came from shows how the Action itself ends. */
|
|
137
|
+
const outcomeLines = (card: ActionCard, words: ReturnType<typeof approvalMessages.resolve>["t"], reopen: () => void) => ({
|
|
138
|
+
sending: { tone: "running" as const, text: [{ kind: "text" as const, text: words.sending }] },
|
|
139
|
+
approved: { tone: "done" as const, text: [{ kind: "text" as const, text: words.approvedHere }], detail: words.continuesInChat },
|
|
140
|
+
rejected: { tone: "off" as const, text: card.forms.rejected, detail: words.declinedHere },
|
|
141
|
+
elsewhere: { tone: "off" as const, icon: "ti ti-info-circle", text: [{ kind: "text" as const, text: words.decidedElsewhere }] },
|
|
142
|
+
gone: { tone: "off" as const, icon: "ti ti-clock-x", text: [{ kind: "text" as const, text: words.gone }] },
|
|
143
|
+
failed: {
|
|
144
|
+
tone: "warning" as const,
|
|
145
|
+
icon: "ti ti-alert-triangle",
|
|
146
|
+
text: [{ kind: "text" as const, text: words.notApplied }],
|
|
147
|
+
detail: words.decisionFailed,
|
|
148
|
+
action: { label: words.answerAgain, onSelect: reopen },
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/** Answers that arm late, so a click meant for what was there before cannot approve a request that just appeared. */
|
|
153
|
+
function Armed(props: { children: JSX.Element }) {
|
|
154
|
+
const [armed, setArmed] = createSignal(false);
|
|
155
|
+
onMount(() => {
|
|
156
|
+
const timer = setTimeout(() => setArmed(true), ARM_DELAY_MS);
|
|
157
|
+
onCleanup(() => clearTimeout(timer));
|
|
158
|
+
});
|
|
159
|
+
return <div inert={armed() ? undefined : true}>{props.children}</div>;
|
|
190
160
|
}
|
|
191
161
|
|
|
192
|
-
|
|
162
|
+
/** One request as the chat's question card. Its answers arm late, so a click meant for the page cannot approve it. */
|
|
163
|
+
function ApprovalCard(props: {
|
|
164
|
+
item: PendingApproval;
|
|
165
|
+
outcome?: Outcome;
|
|
166
|
+
busy: boolean;
|
|
167
|
+
failed: boolean;
|
|
168
|
+
decide: (input: ApprovalDecisionInput) => void;
|
|
169
|
+
reopen: () => void;
|
|
170
|
+
}) {
|
|
193
171
|
const t = useMessages();
|
|
194
|
-
const
|
|
172
|
+
const locale = useLocale();
|
|
173
|
+
const card = createMemo(() => approvalCard(props.item, locale(), READER_TIME_ZONE));
|
|
174
|
+
const current = () => props.outcome ?? (props.failed ? "failed" : "sending");
|
|
175
|
+
const state = () => (props.outcome || props.busy || props.failed ? "decided" : "open");
|
|
176
|
+
// `confirm` asks every time, whatever the stored options say.
|
|
177
|
+
const remember = () =>
|
|
178
|
+
props.item.remember.chat && props.item.assurance === "standard"
|
|
179
|
+
? {
|
|
180
|
+
label: props.item.remember.website ? actionMessages(card().locale).rememberWebsite : actionMessages(card().locale).remember,
|
|
181
|
+
onSelect: () => props.decide({ approved: true, remember: "chat" }),
|
|
182
|
+
}
|
|
183
|
+
: undefined;
|
|
195
184
|
return (
|
|
196
|
-
<
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
185
|
+
<article class="cloud-approval" data-decided={props.outcome} aria-label={t().approvalFor({ title: card().questionText })}>
|
|
186
|
+
<Origin item={props.item} />
|
|
187
|
+
<Armed>
|
|
188
|
+
<ActionQuestion
|
|
189
|
+
card={card()}
|
|
190
|
+
state={state()}
|
|
191
|
+
timeZone={READER_TIME_ZONE}
|
|
192
|
+
remember={remember()}
|
|
193
|
+
onConfirm={() => props.decide({ approved: true })}
|
|
194
|
+
onDecline={() => props.decide({ approved: false })}
|
|
195
|
+
// The outcome lines join the card's sentence, so they speak the stored review's language.
|
|
196
|
+
outcome={
|
|
197
|
+
<ActionReceipt
|
|
198
|
+
card={card()}
|
|
199
|
+
lines={outcomeLines(card(), approvalMessages.resolve([card().locale]).t, props.reopen)}
|
|
200
|
+
current={current()}
|
|
201
|
+
/>
|
|
202
|
+
}
|
|
203
|
+
/>
|
|
204
|
+
</Armed>
|
|
205
|
+
</article>
|
|
200
206
|
);
|
|
201
207
|
}
|
|
202
208
|
|
|
203
|
-
/** Reject, then approve
|
|
204
|
-
function
|
|
209
|
+
/** Reject, then approve, for the compact band; the full card opens in a panel. */
|
|
210
|
+
function BandDecision(props: {
|
|
211
|
+
item: PendingApproval;
|
|
212
|
+
busy: boolean;
|
|
213
|
+
details: () => void;
|
|
214
|
+
decide: (input: ApprovalDecisionInput) => void;
|
|
215
|
+
}) {
|
|
205
216
|
const t = useMessages();
|
|
206
217
|
const [armed, setArmed] = createSignal(false);
|
|
207
218
|
onMount(() => {
|
|
208
219
|
const timer = setTimeout(() => setArmed(true), ARM_DELAY_MS);
|
|
209
220
|
onCleanup(() => clearTimeout(timer));
|
|
210
221
|
});
|
|
211
|
-
const rememberItems = (): DropdownItem[] => [
|
|
212
|
-
...(props.item.remember.chat
|
|
213
|
-
? [
|
|
214
|
-
{
|
|
215
|
-
label: props.item.remember.website ? t().allowWebsiteForChat : t().approveForChat,
|
|
216
|
-
icon: props.item.remember.website ? "ti ti-world-check" : "ti ti-message-check",
|
|
217
|
-
action: () => props.decide({ approved: true, remember: "chat" }),
|
|
218
|
-
},
|
|
219
|
-
]
|
|
220
|
-
: []),
|
|
221
|
-
...(props.item.remember.always
|
|
222
|
-
? [{ label: t().alwaysApprove, icon: "ti ti-shield-check", action: () => props.decide({ approved: true, remember: "always" }) }]
|
|
223
|
-
: []),
|
|
224
|
-
];
|
|
225
222
|
return (
|
|
226
223
|
// Inert, not disabled, while arming: the buttons look the same from the first frame, so nothing flickers.
|
|
227
224
|
<div class="cloud-approval__actions" inert={armed() ? undefined : true}>
|
|
228
|
-
<
|
|
229
|
-
{(
|
|
230
|
-
|
|
231
|
-
{t().details}
|
|
232
|
-
</Button>
|
|
233
|
-
)}
|
|
234
|
-
</Show>
|
|
225
|
+
<Button size="sm" variant="ghost" onClick={() => props.details()}>
|
|
226
|
+
{t().details}
|
|
227
|
+
</Button>
|
|
235
228
|
<Button size="sm" variant="ghost" disabled={props.busy} onClick={() => props.decide({ approved: false })}>
|
|
236
229
|
{t().reject}
|
|
237
230
|
</Button>
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
<Button size="sm" variant="primary" loading={props.busy} onClick={() => props.decide({ approved: true })}>
|
|
242
|
-
{t().approve}
|
|
243
|
-
</Button>
|
|
244
|
-
}
|
|
245
|
-
>
|
|
246
|
-
<SplitButton
|
|
247
|
-
size="sm"
|
|
248
|
-
variant="primary"
|
|
249
|
-
loading={props.busy}
|
|
250
|
-
menuLabel={t().moreOptions}
|
|
251
|
-
menuPosition="bottom-right"
|
|
252
|
-
items={rememberItems()}
|
|
253
|
-
onClick={() => props.decide({ approved: true })}
|
|
254
|
-
>
|
|
255
|
-
{t().approve}
|
|
256
|
-
</SplitButton>
|
|
257
|
-
</Show>
|
|
231
|
+
<Button size="sm" variant="primary" loading={props.busy} onClick={() => props.decide({ approved: true })}>
|
|
232
|
+
{t().approve}
|
|
233
|
+
</Button>
|
|
258
234
|
</div>
|
|
259
235
|
);
|
|
260
236
|
}
|
|
261
237
|
|
|
262
|
-
function
|
|
238
|
+
function Receipt(props: { outcome: Outcome }) {
|
|
263
239
|
const t = useMessages();
|
|
264
|
-
const
|
|
265
|
-
({
|
|
266
|
-
open: t().openIn({ app: props.item.app.name }),
|
|
267
|
-
edit: t().editIn({ app: props.item.app.name }),
|
|
268
|
-
status: t().statusIn({ app: props.item.app.name }),
|
|
269
|
-
preview: t().preview,
|
|
270
|
-
download: t().download,
|
|
271
|
-
})[rel];
|
|
272
|
-
return (
|
|
273
|
-
<Show when={(props.item.review?.links ?? []).length > 0}>
|
|
274
|
-
<nav class="cloud-approval__links" aria-label={t().approvalFor({ title: approvalTitle(props.item) })}>
|
|
275
|
-
<For each={props.item.review?.links ?? []}>
|
|
276
|
-
{(link) => (
|
|
277
|
-
<ButtonLink href={link.href} size="sm" variant="ghost">
|
|
278
|
-
<i class="ti ti-external-link" aria-hidden="true" />
|
|
279
|
-
{link.title ?? title(link.rel)}
|
|
280
|
-
</ButtonLink>
|
|
281
|
-
)}
|
|
282
|
-
</For>
|
|
283
|
-
</nav>
|
|
284
|
-
</Show>
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function BlockDetails(props: { item: PendingApproval; clamp?: boolean }) {
|
|
289
|
-
const locale = useLocale();
|
|
240
|
+
const label = () => ({ approved: t().approved, rejected: t().rejected, elsewhere: t().decidedElsewhere, gone: t().gone })[props.outcome];
|
|
290
241
|
return (
|
|
291
|
-
<
|
|
292
|
-
{(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
<h4 class="cloud-approval__label">{detail.label}</h4>
|
|
296
|
-
</Show>
|
|
297
|
-
<Show
|
|
298
|
-
when={detail.format === "markdown" && !props.clamp}
|
|
299
|
-
fallback={
|
|
300
|
-
<p class="cloud-approval__quote" data-clamp={props.clamp ? "true" : undefined}>
|
|
301
|
-
<span>{detailText(detail, locale())}</span>
|
|
302
|
-
</p>
|
|
303
|
-
}
|
|
304
|
-
>
|
|
305
|
-
<MarkdownView class="cloud-approval__markdown" markdown={detail.value} headingScale="compact" allowImages={false} />
|
|
306
|
-
</Show>
|
|
307
|
-
</section>
|
|
308
|
-
)}
|
|
309
|
-
</For>
|
|
242
|
+
<span class="cloud-approval__receipt" data-outcome={props.outcome}>
|
|
243
|
+
<i class={outcomeIcon(props.outcome)} aria-hidden="true" />
|
|
244
|
+
{label()}
|
|
245
|
+
</span>
|
|
310
246
|
);
|
|
311
247
|
}
|
|
312
248
|
|
|
313
|
-
/** Opens the full request in a panel; resolves with the decision made there, if any. */
|
|
249
|
+
/** Opens the full request in a panel as the chat's card; resolves with the decision made there, if any. */
|
|
314
250
|
const openDetails = (item: PendingApproval, locale: string) =>
|
|
315
251
|
dialogCore.open<ApprovalDecisionInput>(
|
|
316
252
|
(close) => {
|
|
317
253
|
const t = () => approvalMessages.resolve([locale]).t;
|
|
254
|
+
const card = approvalCard(item, locale, READER_TIME_ZONE);
|
|
318
255
|
return (
|
|
319
256
|
<PanelDialog>
|
|
320
257
|
<PanelDialog.Header
|
|
@@ -326,25 +263,30 @@ const openDetails = (item: PendingApproval, locale: string) =>
|
|
|
326
263
|
/>
|
|
327
264
|
<PanelDialog.Body>
|
|
328
265
|
<div class="cloud-approval__sheet">
|
|
329
|
-
<
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
266
|
+
<Armed>
|
|
267
|
+
<ActionQuestion
|
|
268
|
+
card={card}
|
|
269
|
+
state="open"
|
|
270
|
+
timeZone={READER_TIME_ZONE}
|
|
271
|
+
remember={
|
|
272
|
+
item.remember.chat && item.assurance === "standard"
|
|
273
|
+
? {
|
|
274
|
+
label: item.remember.website ? actionMessages(card.locale).rememberWebsite : actionMessages(card.locale).remember,
|
|
275
|
+
onSelect: () => close({ approved: true, remember: "chat" }),
|
|
276
|
+
}
|
|
277
|
+
: undefined
|
|
278
|
+
}
|
|
279
|
+
onConfirm={() => close({ approved: true })}
|
|
280
|
+
onDecline={() => close({ approved: false })}
|
|
281
|
+
outcome={null}
|
|
282
|
+
/>
|
|
283
|
+
</Armed>
|
|
334
284
|
<p class="cloud-approval__callout">
|
|
335
285
|
<i class="ti ti-info-circle" aria-hidden="true" />
|
|
336
286
|
{t().exactlyThis}
|
|
337
287
|
</p>
|
|
338
288
|
</div>
|
|
339
289
|
</PanelDialog.Body>
|
|
340
|
-
<PanelDialog.Footer>
|
|
341
|
-
<div class="cloud-approval__sheet-foot">
|
|
342
|
-
<div class="cloud-approval__foot">
|
|
343
|
-
<Note item={item} />
|
|
344
|
-
<Decision item={item} busy={false} decide={(input) => close(input)} />
|
|
345
|
-
</div>
|
|
346
|
-
</div>
|
|
347
|
-
</PanelDialog.Footer>
|
|
348
290
|
</PanelDialog>
|
|
349
291
|
);
|
|
350
292
|
},
|
|
@@ -358,6 +300,7 @@ function Row(props: {
|
|
|
358
300
|
busy: boolean;
|
|
359
301
|
failed: boolean;
|
|
360
302
|
decide: (input: ApprovalDecisionInput) => void;
|
|
303
|
+
reopen: () => void;
|
|
361
304
|
}) {
|
|
362
305
|
const t = useMessages();
|
|
363
306
|
const locale = useLocale();
|
|
@@ -365,37 +308,18 @@ function Row(props: {
|
|
|
365
308
|
const input = await openDetails(props.item, locale());
|
|
366
309
|
if (input) props.decide(input);
|
|
367
310
|
};
|
|
368
|
-
const foot = () => (
|
|
369
|
-
<>
|
|
370
|
-
<Note item={props.item} outcome={props.outcome} failed={props.failed} />
|
|
371
|
-
<Show when={props.outcome} fallback={<Decision item={props.item} busy={props.busy} details={details} decide={props.decide} />}>
|
|
372
|
-
{(outcome) => (
|
|
373
|
-
<div class="cloud-approval__actions">
|
|
374
|
-
<Receipt outcome={outcome()} />
|
|
375
|
-
</div>
|
|
376
|
-
)}
|
|
377
|
-
</Show>
|
|
378
|
-
</>
|
|
379
|
-
);
|
|
380
311
|
return (
|
|
381
312
|
<Show
|
|
382
313
|
when={props.layout === "band"}
|
|
383
314
|
fallback={
|
|
384
|
-
<
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
<Description item={props.item} />
|
|
393
|
-
<Fields item={props.item} />
|
|
394
|
-
<BlockDetails item={props.item} clamp />
|
|
395
|
-
</div>
|
|
396
|
-
<div class="cloud-approval__foot">{foot()}</div>
|
|
397
|
-
</div>
|
|
398
|
-
</article>
|
|
315
|
+
<ApprovalCard
|
|
316
|
+
item={props.item}
|
|
317
|
+
outcome={props.outcome}
|
|
318
|
+
busy={props.busy}
|
|
319
|
+
failed={props.failed}
|
|
320
|
+
decide={props.decide}
|
|
321
|
+
reopen={props.reopen}
|
|
322
|
+
/>
|
|
399
323
|
}
|
|
400
324
|
>
|
|
401
325
|
<article class="cloud-approval-band" data-decided={props.outcome} aria-label={t().approvalFor({ title: approvalTitle(props.item) })}>
|
|
@@ -404,7 +328,24 @@ function Row(props: {
|
|
|
404
328
|
<strong>{approvalTitle(props.item)}</strong>
|
|
405
329
|
<Origin item={props.item} withApp={false} />
|
|
406
330
|
</div>
|
|
407
|
-
<div class="cloud-approval__foot">
|
|
331
|
+
<div class="cloud-approval__foot">
|
|
332
|
+
<Show when={props.failed && !props.outcome}>
|
|
333
|
+
<span class="cloud-approval__note" data-tone="danger" role="alert">
|
|
334
|
+
<i class="ti ti-alert-circle" aria-hidden="true" />
|
|
335
|
+
<span>{t().decisionFailed}</span>
|
|
336
|
+
</span>
|
|
337
|
+
</Show>
|
|
338
|
+
<Show
|
|
339
|
+
when={props.outcome}
|
|
340
|
+
fallback={<BandDecision item={props.item} busy={props.busy} details={() => void details()} decide={props.decide} />}
|
|
341
|
+
>
|
|
342
|
+
{(outcome) => (
|
|
343
|
+
<div class="cloud-approval__actions">
|
|
344
|
+
<Receipt outcome={outcome()} />
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
</Show>
|
|
348
|
+
</div>
|
|
408
349
|
</article>
|
|
409
350
|
</Show>
|
|
410
351
|
);
|
|
@@ -528,6 +469,7 @@ export default function Approvals(props: ApprovalsProps) {
|
|
|
528
469
|
busy={busy().has(item.id)}
|
|
529
470
|
failed={failed().has(item.id)}
|
|
530
471
|
decide={(input) => void decide(item, input)}
|
|
472
|
+
reopen={() => setFailed((current) => toggle(current, item.id, false))}
|
|
531
473
|
/>
|
|
532
474
|
)}
|
|
533
475
|
</For>
|
|
@@ -21,27 +21,21 @@ export const approvalMessages = i18n.define({
|
|
|
21
21
|
allApprovals: "All approvals",
|
|
22
22
|
fromChat: ({ title }: { title: string }) => `from chat “${title}”`,
|
|
23
23
|
fromUntitledChat: "from a chat",
|
|
24
|
-
asksEveryTime: "Asks every time",
|
|
25
24
|
expiresIn: ({ time }: { time: string }) => `Expires ${time}`,
|
|
26
25
|
details: "Details",
|
|
27
26
|
reject: "Reject",
|
|
28
27
|
approve: "Approve",
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
notApplied: "Your decision was not applied.",
|
|
29
|
+
sending: "Sending your answer …",
|
|
30
|
+
approvedHere: "Approved.",
|
|
31
|
+
continuesInChat: "The chat it came from carries it out and shows how it ends.",
|
|
32
|
+
declinedHere: "You declined.",
|
|
33
|
+
answerAgain: "Answer again",
|
|
35
34
|
approved: "Approved",
|
|
36
35
|
rejected: "Rejected",
|
|
37
36
|
decidedElsewhere: "Already decided elsewhere",
|
|
38
37
|
gone: "Expired or withdrawn",
|
|
39
38
|
decisionFailed: "The decision was not saved. Try again.",
|
|
40
|
-
openIn: ({ app }: { app: string }) => `Open in ${app}`,
|
|
41
|
-
editIn: ({ app }: { app: string }) => `Edit in ${app}`,
|
|
42
|
-
statusIn: ({ app }: { app: string }) => `Status in ${app}`,
|
|
43
|
-
preview: "Preview",
|
|
44
|
-
download: "Download",
|
|
45
39
|
exactlyThis: "This approval applies to exactly this Action. If it changes before it runs, Cloud asks again.",
|
|
46
40
|
approvalFor: ({ title }: { title: string }) => `Approval: ${title}`,
|
|
47
41
|
close: "Close",
|
|
@@ -65,27 +59,21 @@ export const approvalMessages = i18n.define({
|
|
|
65
59
|
allApprovals: "Alle Freigaben",
|
|
66
60
|
fromChat: ({ title }) => `aus Chat „${title}“`,
|
|
67
61
|
fromUntitledChat: "aus einem Chat",
|
|
68
|
-
asksEveryTime: "Fragt immer",
|
|
69
62
|
expiresIn: ({ time }) => `Läuft ab ${time}`,
|
|
70
63
|
details: "Details",
|
|
71
64
|
reject: "Ablehnen",
|
|
72
65
|
approve: "Freigeben",
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
notApplied: "Deine Entscheidung wurde nicht übernommen.",
|
|
67
|
+
sending: "Deine Antwort wird gesendet …",
|
|
68
|
+
approvedHere: "Freigegeben.",
|
|
69
|
+
continuesInChat: "Der Chat, aus dem sie kommt, führt sie aus und zeigt, wie sie endet.",
|
|
70
|
+
declinedHere: "Du hast abgelehnt.",
|
|
71
|
+
answerAgain: "Noch einmal antworten",
|
|
79
72
|
approved: "Freigegeben",
|
|
80
73
|
rejected: "Abgelehnt",
|
|
81
74
|
decidedElsewhere: "Schon anderswo entschieden",
|
|
82
75
|
gone: "Abgelaufen oder zurückgezogen",
|
|
83
76
|
decisionFailed: "Die Entscheidung wurde nicht gespeichert. Versuche es erneut.",
|
|
84
|
-
openIn: ({ app }) => `In ${app} öffnen`,
|
|
85
|
-
editIn: ({ app }) => `In ${app} bearbeiten`,
|
|
86
|
-
statusIn: ({ app }) => `Status in ${app}`,
|
|
87
|
-
preview: "Vorschau",
|
|
88
|
-
download: "Herunterladen",
|
|
89
77
|
exactlyThis: "Diese Freigabe gilt genau für diese Aktion. Ändert sie sich, bevor sie läuft, fragt Cloud neu.",
|
|
90
78
|
approvalFor: ({ title }) => `Freigabe: ${title}`,
|
|
91
79
|
close: "Schließen",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appAccentStyle } from "../ssr/app-appearance";
|
|
1
2
|
import { createGlobalSearchHost } from "../ssr/global-search-host";
|
|
2
3
|
import { openCloudResourcePicker } from "./resource-picker";
|
|
3
4
|
|
|
@@ -7,5 +8,12 @@ import { openCloudResourcePicker } from "./resource-picker";
|
|
|
7
8
|
*/
|
|
8
9
|
const params = new URLSearchParams(location.search);
|
|
9
10
|
const app = params.get("app") ?? undefined;
|
|
10
|
-
if (params.has("picker"))
|
|
11
|
-
|
|
11
|
+
if (params.has("picker")) {
|
|
12
|
+
// The page shell renders `<body class="k2b-ui app-accent-scope" style="…">` for an app page.
|
|
13
|
+
const style = appAccentStyle(params.get("accent") ?? undefined);
|
|
14
|
+
if (style) {
|
|
15
|
+
document.body.classList.add("app-accent-scope");
|
|
16
|
+
document.body.style.cssText = style;
|
|
17
|
+
}
|
|
18
|
+
void openCloudResourcePicker({ initialAppId: app });
|
|
19
|
+
} else createGlobalSearchHost(() => []).open(app ? { scope: { appId: app, label: params.get("label") ?? app } } : {});
|