@kontainer/strapi-plugin 0.1.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/admin/KontainerMediaInput-C6wPHxqb.js +148 -0
  4. package/dist/admin/KontainerMediaInput-DmEdEpue.mjs +130 -0
  5. package/dist/admin/Settings-fZTH8z6T.js +152 -0
  6. package/dist/admin/Settings-tNEBWX1o.mjs +134 -0
  7. package/dist/admin/da-Be8uFPrt.js +28 -0
  8. package/dist/admin/da-WHaTm2Cy.mjs +28 -0
  9. package/dist/admin/en-B3JWZj1Z.mjs +28 -0
  10. package/dist/admin/en-B7EZhOY4.js +28 -0
  11. package/dist/admin/index-CMAfPTry.js +121 -0
  12. package/dist/admin/index-hpPTiq45.mjs +122 -0
  13. package/dist/admin/index.js +4 -0
  14. package/dist/admin/index.mjs +4 -0
  15. package/dist/admin/src/components/Initializer.d.ts +5 -0
  16. package/dist/admin/src/components/KontainerMediaInput.d.ts +3 -0
  17. package/dist/admin/src/components/PluginIcon.d.ts +2 -0
  18. package/dist/admin/src/index.d.ts +3 -0
  19. package/dist/admin/src/pages/Settings.d.ts +3 -0
  20. package/dist/admin/src/pluginId.d.ts +1 -0
  21. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  22. package/dist/server/index.js +356 -0
  23. package/dist/server/index.mjs +356 -0
  24. package/dist/server/src/bootstrap.d.ts +5 -0
  25. package/dist/server/src/config/index.d.ts +9 -0
  26. package/dist/server/src/content-types/index.d.ts +2 -0
  27. package/dist/server/src/controllers/controller.d.ts +11 -0
  28. package/dist/server/src/controllers/index.d.ts +12 -0
  29. package/dist/server/src/destroy.d.ts +5 -0
  30. package/dist/server/src/index.d.ts +108 -0
  31. package/dist/server/src/middlewares/index.d.ts +2 -0
  32. package/dist/server/src/policies/index.d.ts +2 -0
  33. package/dist/server/src/register.d.ts +5 -0
  34. package/dist/server/src/routes/admin/index.d.ts +12 -0
  35. package/dist/server/src/routes/content-api/index.d.ts +12 -0
  36. package/dist/server/src/routes/index.d.ts +25 -0
  37. package/dist/server/src/services/index.d.ts +51 -0
  38. package/dist/server/src/services/service.d.ts +57 -0
  39. package/package.json +91 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kontainer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # Strapi plugin: Kontainer DAM
2
+
3
+ [Kontainer](https://kontainer.com) integration for **Strapi 5**. Content
4
+ editors pick images, videos and files straight from your Kontainer DAM inside
5
+ the Strapi editor — Strapi stores only a JSON reference to the asset, never
6
+ the file. Kontainer stays the single source of truth.
7
+
8
+ ## Features
9
+
10
+ - **`kontainer.media` custom field** — a "Choose from Kontainer" button on any
11
+ content type opens the Kontainer file picker. Selecting a file stores its
12
+ reference on the entry:
13
+ - `fileId`, `fileName`, `folderId` — identifiers for retrieving the asset
14
+ - `url` — CDN URL, including the chosen **download template**
15
+ (`?d=<templateId>`), plus `urlBaseName` and `originalUrl`
16
+ - `thumbnailUrl`, `type`, `extension`, dimensions and size
17
+ - **all texts**: `alt`, `description` and every Kontainer custom field
18
+ (`cf`) — stored in the JSON alongside the reference
19
+ - **Download templates** — editors apply crop/resize/format templates directly
20
+ in the picker; the resulting CDN URL and template id are stored.
21
+ - **File usage endpoint** — `GET /api/kontainer/usage/:fileId` reports every
22
+ entry referencing a Kontainer file, across drafts, published versions,
23
+ locales, components and dynamic zones. Lets Kontainer show where a file is
24
+ used before it is changed or deleted.
25
+ - **No duplication** — nothing is uploaded to the Strapi media library.
26
+
27
+ ## Requirements
28
+
29
+ - Strapi v5 (built and tested on 5.x with the Plugin SDK)
30
+ - A Kontainer account (`https://yourcompany.kontainer.com`)
31
+
32
+ ## Installation
33
+
34
+ ```sh
35
+ npm install @kontainer/strapi-plugin
36
+ # or
37
+ yarn add @kontainer/strapi-plugin
38
+ ```
39
+
40
+ ## Configuration
41
+
42
+ Open **Settings → Kontainer → Configuration** in the admin panel and enter
43
+ your Kontainer URL (e.g. `https://yourcompany.kontainer.com`). That's it — no
44
+ code changes required.
45
+
46
+ The plugin automatically extends the admin panel's Content-Security-Policy so
47
+ thumbnails from `*.kontainer.com` (and any URL configured via file config)
48
+ load in the editor.
49
+
50
+ <details>
51
+ <summary>Optional: configure via environment instead</summary>
52
+
53
+ The admin-panel setting wins; this is a fallback for infrastructure-as-code
54
+ setups:
55
+
56
+ ```ts
57
+ // config/plugins.ts
58
+ export default ({ env }) => ({
59
+ kontainer: {
60
+ enabled: true,
61
+ config: {
62
+ url: env('KONTAINER_URL', ''),
63
+ },
64
+ },
65
+ });
66
+ ```
67
+
68
+ If you use a custom (non-`kontainer.com`) domain configured only through the
69
+ admin panel, add it to `img-src`/`media-src` in `config/middlewares.ts` — the
70
+ plugin logs a warning at startup when this applies.
71
+
72
+ </details>
73
+
74
+ ## Usage
75
+
76
+ ### Add the field to a content type
77
+
78
+ In the Content-Type Builder choose **Custom fields → Kontainer media**, or add
79
+ it to the schema directly:
80
+
81
+ ```json
82
+ "hero": {
83
+ "type": "customField",
84
+ "customField": "plugin::kontainer.media"
85
+ }
86
+ ```
87
+
88
+ ### Consume from your frontend
89
+
90
+ The stored reference is returned as-is by the Strapi content API:
91
+
92
+ ```ts
93
+ const page = await fetch(`${STRAPI_URL}/api/pages/${documentId}`).then((r) => r.json());
94
+ const { url, alt } = page.data.hero; // request the asset from Kontainer's CDN
95
+ ```
96
+
97
+ ### File usage
98
+
99
+ ```
100
+ GET /api/kontainer/usage/:fileId
101
+ Authorization: Bearer <strapi api token>
102
+
103
+ {
104
+ "fileId": "5764",
105
+ "count": 1,
106
+ "data": [
107
+ {
108
+ "contentType": "api::page.page",
109
+ "field": "hero",
110
+ "documentId": "m3q2h...",
111
+ "id": 1,
112
+ "locale": null,
113
+ "status": "published"
114
+ }
115
+ ]
116
+ }
117
+ ```
118
+
119
+ ## Development
120
+
121
+ ```sh
122
+ npm install
123
+ npm run watch # rebuild on change
124
+ npm run verify # validate the package before publishing
125
+ ```
126
+
127
+ ## License
128
+
129
+ [MIT](./LICENSE) © Kontainer
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const React = require("react");
5
+ const designSystem = require("@strapi/design-system");
6
+ const admin = require("@strapi/strapi/admin");
7
+ const reactIntl = require("react-intl");
8
+ const index = require("./index-CMAfPTry.js");
9
+ function _interopNamespace(e) {
10
+ if (e && e.__esModule) return e;
11
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
12
+ if (e) {
13
+ for (const k in e) {
14
+ if (k !== "default") {
15
+ const d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(n, k, d.get ? d : {
17
+ enumerable: true,
18
+ get: () => e[k]
19
+ });
20
+ }
21
+ }
22
+ }
23
+ n.default = e;
24
+ return Object.freeze(n);
25
+ }
26
+ const React__namespace = /* @__PURE__ */ _interopNamespace(React);
27
+ const PICKER_WINDOW_FEATURES = "location=yes,height=800,width=1200,scrollbars=yes,status=yes";
28
+ const toFiles = (value) => {
29
+ let parsed = null;
30
+ if (typeof value === "string") {
31
+ try {
32
+ parsed = JSON.parse(value);
33
+ } catch {
34
+ parsed = null;
35
+ }
36
+ } else if (value && typeof value === "object") {
37
+ parsed = value;
38
+ }
39
+ if (!parsed) return [];
40
+ return Array.isArray(parsed) ? parsed : [parsed];
41
+ };
42
+ const KontainerMediaInput = React__namespace.forwardRef(
43
+ ({ name, value, onChange, error, hint, label, labelAction, required, disabled }, ref) => {
44
+ const { formatMessage } = reactIntl.useIntl();
45
+ const { get } = admin.useFetchClient();
46
+ const [kontainerUrl, setKontainerUrl] = React__namespace.useState(null);
47
+ const messageHandler = React__namespace.useRef(null);
48
+ React__namespace.useEffect(() => {
49
+ let mounted = true;
50
+ get("/kontainer/config").then(({ data }) => {
51
+ if (mounted) setKontainerUrl((data.url ?? "").replace(/\/+$/, ""));
52
+ }).catch(() => {
53
+ if (mounted) setKontainerUrl("");
54
+ });
55
+ return () => {
56
+ mounted = false;
57
+ if (messageHandler.current) {
58
+ window.removeEventListener("message", messageHandler.current);
59
+ }
60
+ };
61
+ }, [get]);
62
+ const files = toFiles(value);
63
+ const openPicker = () => {
64
+ if (!kontainerUrl) return;
65
+ if (messageHandler.current) {
66
+ window.removeEventListener("message", messageHandler.current);
67
+ }
68
+ const picker = window.open(
69
+ `${kontainerUrl}/?cmsMode=1`,
70
+ "kontainer-picker",
71
+ PICKER_WINDOW_FEATURES
72
+ );
73
+ const handler = (event) => {
74
+ if (event.origin !== new URL(kontainerUrl).origin) return;
75
+ let data = event.data;
76
+ if (typeof data === "string") {
77
+ try {
78
+ data = JSON.parse(data);
79
+ } catch {
80
+ return;
81
+ }
82
+ }
83
+ if (!data || typeof data !== "object") return;
84
+ window.removeEventListener("message", handler);
85
+ messageHandler.current = null;
86
+ onChange({
87
+ target: { name, value: JSON.stringify(data), type: "json" }
88
+ });
89
+ picker?.close();
90
+ };
91
+ messageHandler.current = handler;
92
+ window.addEventListener("message", handler);
93
+ };
94
+ const clear = () => {
95
+ onChange({ target: { name, value: null, type: "json" } });
96
+ };
97
+ const notConfigured = kontainerUrl === "";
98
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name, error, hint, required, children: [
99
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { action: labelAction, children: label }),
100
+ files.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 4, paddingTop: 2, paddingBottom: 2, wrap: "wrap", children: files.map((file, index2) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, alignItems: "center", children: [
101
+ (file.thumbnailUrl || file.type === "image") && /* @__PURE__ */ jsxRuntime.jsx(
102
+ "img",
103
+ {
104
+ src: file.thumbnailUrl || file.url,
105
+ alt: file.alt || file.fileName || "",
106
+ style: { height: "6.4rem", borderRadius: "4px" }
107
+ }
108
+ ),
109
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
110
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: "semiBold", textColor: "neutral800", children: file.fileName ?? String(file.fileId ?? "") }),
111
+ file.alt ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: file.alt }) : null
112
+ ] })
113
+ ] }, `${file.fileId ?? index2}`)) }),
114
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, paddingTop: files.length ? 0 : 1, children: [
115
+ /* @__PURE__ */ jsxRuntime.jsx(
116
+ designSystem.Button,
117
+ {
118
+ ref,
119
+ onClick: openPicker,
120
+ disabled: disabled || !kontainerUrl,
121
+ variant: "secondary",
122
+ children: formatMessage(
123
+ files.length ? {
124
+ id: index.getTranslation("input.replace"),
125
+ defaultMessage: "Replace"
126
+ } : {
127
+ id: index.getTranslation("input.choose"),
128
+ defaultMessage: "Choose from Kontainer"
129
+ }
130
+ )
131
+ }
132
+ ),
133
+ files.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: clear, disabled, variant: "danger-light", children: formatMessage({
134
+ id: index.getTranslation("input.remove"),
135
+ defaultMessage: "Remove"
136
+ }) })
137
+ ] }),
138
+ notConfigured && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: formatMessage({
139
+ id: index.getTranslation("input.not-configured"),
140
+ defaultMessage: "Kontainer URL is not configured. Set it under Settings → Kontainer."
141
+ }) }),
142
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}),
143
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {})
144
+ ] });
145
+ }
146
+ );
147
+ KontainerMediaInput.displayName = "KontainerMediaInput";
148
+ exports.default = KontainerMediaInput;
@@ -0,0 +1,130 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Field, Flex, Typography, Button } from "@strapi/design-system";
4
+ import { useFetchClient } from "@strapi/strapi/admin";
5
+ import { useIntl } from "react-intl";
6
+ import { g as getTranslation } from "./index-hpPTiq45.mjs";
7
+ const PICKER_WINDOW_FEATURES = "location=yes,height=800,width=1200,scrollbars=yes,status=yes";
8
+ const toFiles = (value) => {
9
+ let parsed = null;
10
+ if (typeof value === "string") {
11
+ try {
12
+ parsed = JSON.parse(value);
13
+ } catch {
14
+ parsed = null;
15
+ }
16
+ } else if (value && typeof value === "object") {
17
+ parsed = value;
18
+ }
19
+ if (!parsed) return [];
20
+ return Array.isArray(parsed) ? parsed : [parsed];
21
+ };
22
+ const KontainerMediaInput = React.forwardRef(
23
+ ({ name, value, onChange, error, hint, label, labelAction, required, disabled }, ref) => {
24
+ const { formatMessage } = useIntl();
25
+ const { get } = useFetchClient();
26
+ const [kontainerUrl, setKontainerUrl] = React.useState(null);
27
+ const messageHandler = React.useRef(null);
28
+ React.useEffect(() => {
29
+ let mounted = true;
30
+ get("/kontainer/config").then(({ data }) => {
31
+ if (mounted) setKontainerUrl((data.url ?? "").replace(/\/+$/, ""));
32
+ }).catch(() => {
33
+ if (mounted) setKontainerUrl("");
34
+ });
35
+ return () => {
36
+ mounted = false;
37
+ if (messageHandler.current) {
38
+ window.removeEventListener("message", messageHandler.current);
39
+ }
40
+ };
41
+ }, [get]);
42
+ const files = toFiles(value);
43
+ const openPicker = () => {
44
+ if (!kontainerUrl) return;
45
+ if (messageHandler.current) {
46
+ window.removeEventListener("message", messageHandler.current);
47
+ }
48
+ const picker = window.open(
49
+ `${kontainerUrl}/?cmsMode=1`,
50
+ "kontainer-picker",
51
+ PICKER_WINDOW_FEATURES
52
+ );
53
+ const handler = (event) => {
54
+ if (event.origin !== new URL(kontainerUrl).origin) return;
55
+ let data = event.data;
56
+ if (typeof data === "string") {
57
+ try {
58
+ data = JSON.parse(data);
59
+ } catch {
60
+ return;
61
+ }
62
+ }
63
+ if (!data || typeof data !== "object") return;
64
+ window.removeEventListener("message", handler);
65
+ messageHandler.current = null;
66
+ onChange({
67
+ target: { name, value: JSON.stringify(data), type: "json" }
68
+ });
69
+ picker?.close();
70
+ };
71
+ messageHandler.current = handler;
72
+ window.addEventListener("message", handler);
73
+ };
74
+ const clear = () => {
75
+ onChange({ target: { name, value: null, type: "json" } });
76
+ };
77
+ const notConfigured = kontainerUrl === "";
78
+ return /* @__PURE__ */ jsxs(Field.Root, { name, error, hint, required, children: [
79
+ /* @__PURE__ */ jsx(Field.Label, { action: labelAction, children: label }),
80
+ files.length > 0 && /* @__PURE__ */ jsx(Flex, { gap: 4, paddingTop: 2, paddingBottom: 2, wrap: "wrap", children: files.map((file, index) => /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", children: [
81
+ (file.thumbnailUrl || file.type === "image") && /* @__PURE__ */ jsx(
82
+ "img",
83
+ {
84
+ src: file.thumbnailUrl || file.url,
85
+ alt: file.alt || file.fileName || "",
86
+ style: { height: "6.4rem", borderRadius: "4px" }
87
+ }
88
+ ),
89
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
90
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", textColor: "neutral800", children: file.fileName ?? String(file.fileId ?? "") }),
91
+ file.alt ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: file.alt }) : null
92
+ ] })
93
+ ] }, `${file.fileId ?? index}`)) }),
94
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, paddingTop: files.length ? 0 : 1, children: [
95
+ /* @__PURE__ */ jsx(
96
+ Button,
97
+ {
98
+ ref,
99
+ onClick: openPicker,
100
+ disabled: disabled || !kontainerUrl,
101
+ variant: "secondary",
102
+ children: formatMessage(
103
+ files.length ? {
104
+ id: getTranslation("input.replace"),
105
+ defaultMessage: "Replace"
106
+ } : {
107
+ id: getTranslation("input.choose"),
108
+ defaultMessage: "Choose from Kontainer"
109
+ }
110
+ )
111
+ }
112
+ ),
113
+ files.length > 0 && /* @__PURE__ */ jsx(Button, { onClick: clear, disabled, variant: "danger-light", children: formatMessage({
114
+ id: getTranslation("input.remove"),
115
+ defaultMessage: "Remove"
116
+ }) })
117
+ ] }),
118
+ notConfigured && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: formatMessage({
119
+ id: getTranslation("input.not-configured"),
120
+ defaultMessage: "Kontainer URL is not configured. Set it under Settings → Kontainer."
121
+ }) }),
122
+ /* @__PURE__ */ jsx(Field.Hint, {}),
123
+ /* @__PURE__ */ jsx(Field.Error, {})
124
+ ] });
125
+ }
126
+ );
127
+ KontainerMediaInput.displayName = "KontainerMediaInput";
128
+ export {
129
+ KontainerMediaInput as default
130
+ };
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const React = require("react");
5
+ const designSystem = require("@strapi/design-system");
6
+ const icons = require("@strapi/icons");
7
+ const admin = require("@strapi/strapi/admin");
8
+ const reactIntl = require("react-intl");
9
+ const index = require("./index-CMAfPTry.js");
10
+ function _interopNamespace(e) {
11
+ if (e && e.__esModule) return e;
12
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
13
+ if (e) {
14
+ for (const k in e) {
15
+ if (k !== "default") {
16
+ const d = Object.getOwnPropertyDescriptor(e, k);
17
+ Object.defineProperty(n, k, d.get ? d : {
18
+ enumerable: true,
19
+ get: () => e[k]
20
+ });
21
+ }
22
+ }
23
+ }
24
+ n.default = e;
25
+ return Object.freeze(n);
26
+ }
27
+ const React__namespace = /* @__PURE__ */ _interopNamespace(React);
28
+ const VALIDATION_DEBOUNCE_MS = 500;
29
+ const Settings = () => {
30
+ const { formatMessage } = reactIntl.useIntl();
31
+ const { get, put } = admin.useFetchClient();
32
+ const { toggleNotification } = admin.useNotification();
33
+ const [url, setUrl] = React__namespace.useState("");
34
+ const [fileUrl, setFileUrl] = React__namespace.useState("");
35
+ const [loading, setLoading] = React__namespace.useState(true);
36
+ const [saving, setSaving] = React__namespace.useState(false);
37
+ const [validation, setValidation] = React__namespace.useState({ status: "idle" });
38
+ const validationSeq = React__namespace.useRef(0);
39
+ React__namespace.useEffect(() => {
40
+ get("/kontainer/settings").then(({ data }) => {
41
+ setUrl(data.url);
42
+ setFileUrl(data.fileUrl);
43
+ }).catch(() => {
44
+ }).finally(() => setLoading(false));
45
+ }, [get]);
46
+ React__namespace.useEffect(() => {
47
+ if (loading) return;
48
+ const seq = ++validationSeq.current;
49
+ const trimmed = url.trim();
50
+ if (!trimmed) {
51
+ setValidation({ status: "idle" });
52
+ return;
53
+ }
54
+ setValidation({ status: "checking" });
55
+ const timer = setTimeout(() => {
56
+ get("/kontainer/settings/validate", { params: { url: trimmed } }).then(({ data }) => {
57
+ if (seq !== validationSeq.current) return;
58
+ setValidation(
59
+ data.valid ? { status: "valid" } : { status: "invalid", reason: data.reason ?? "invalid-url" }
60
+ );
61
+ }).catch(() => {
62
+ if (seq !== validationSeq.current) return;
63
+ setValidation({ status: "invalid", reason: "unreachable" });
64
+ });
65
+ }, VALIDATION_DEBOUNCE_MS);
66
+ return () => clearTimeout(timer);
67
+ }, [url, loading, get]);
68
+ const save = async () => {
69
+ setSaving(true);
70
+ try {
71
+ await put("/kontainer/settings", { url: url.trim() });
72
+ toggleNotification({
73
+ type: "success",
74
+ message: formatMessage({
75
+ id: index.getTranslation("settings.saved"),
76
+ defaultMessage: "Kontainer settings saved"
77
+ })
78
+ });
79
+ } catch {
80
+ toggleNotification({
81
+ type: "danger",
82
+ message: formatMessage({
83
+ id: index.getTranslation("settings.save-error"),
84
+ defaultMessage: "Could not save — enter a valid http(s) URL"
85
+ })
86
+ });
87
+ } finally {
88
+ setSaving(false);
89
+ }
90
+ };
91
+ const invalidMessage = (reason) => formatMessage({
92
+ id: index.getTranslation(`settings.validation.${reason}`),
93
+ defaultMessage: reason === "not-kontainer" ? "This URL does not appear to be a Kontainer instance" : reason === "unreachable" ? "The Strapi server could not reach this URL" : "Not a valid URL"
94
+ });
95
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 10, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "flex-start", gap: 6, maxWidth: "60rem", children: [
96
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
97
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", tag: "h1", children: "Kontainer" }),
98
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "epsilon", textColor: "neutral600", children: formatMessage({
99
+ id: index.getTranslation("settings.subtitle"),
100
+ defaultMessage: "Connect Strapi to your Kontainer DAM. Editors pick files from this Kontainer in the content editor."
101
+ }) })
102
+ ] }),
103
+ /* @__PURE__ */ jsxRuntime.jsxs(
104
+ designSystem.Field.Root,
105
+ {
106
+ name: "kontainer-url",
107
+ error: validation.status === "invalid" ? invalidMessage(validation.reason) : void 0,
108
+ hint: fileUrl ? formatMessage(
109
+ {
110
+ id: index.getTranslation("settings.url.hint-fallback"),
111
+ defaultMessage: "Leave empty to use the value from config/plugins: {fileUrl}"
112
+ },
113
+ { fileUrl }
114
+ ) : formatMessage({
115
+ id: index.getTranslation("settings.url.hint"),
116
+ defaultMessage: "Example: https://yourcompany.kontainer.com"
117
+ }),
118
+ style: { width: "100%" },
119
+ children: [
120
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: formatMessage({
121
+ id: index.getTranslation("settings.url.label"),
122
+ defaultMessage: "Kontainer URL"
123
+ }) }),
124
+ /* @__PURE__ */ jsxRuntime.jsx(
125
+ designSystem.TextInput,
126
+ {
127
+ placeholder: "https://yourcompany.kontainer.com",
128
+ value: url,
129
+ disabled: loading,
130
+ onChange: (e) => setUrl(e.target.value)
131
+ }
132
+ ),
133
+ validation.status === "checking" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({
134
+ id: index.getTranslation("settings.validation.checking"),
135
+ defaultMessage: "Checking…"
136
+ }) }),
137
+ validation.status === "valid" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "success600", children: formatMessage({
138
+ id: index.getTranslation("settings.validation.valid"),
139
+ defaultMessage: "Kontainer instance detected"
140
+ }) }),
141
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}),
142
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {})
143
+ ]
144
+ }
145
+ ),
146
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: save, loading: saving, disabled: loading, startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Check, {}), children: formatMessage({
147
+ id: index.getTranslation("settings.save"),
148
+ defaultMessage: "Save"
149
+ }) })
150
+ ] }) });
151
+ };
152
+ exports.default = Settings;