@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.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/admin/KontainerMediaInput-C6wPHxqb.js +148 -0
- package/dist/admin/KontainerMediaInput-DmEdEpue.mjs +130 -0
- package/dist/admin/Settings-fZTH8z6T.js +152 -0
- package/dist/admin/Settings-tNEBWX1o.mjs +134 -0
- package/dist/admin/da-Be8uFPrt.js +28 -0
- package/dist/admin/da-WHaTm2Cy.mjs +28 -0
- package/dist/admin/en-B3JWZj1Z.mjs +28 -0
- package/dist/admin/en-B7EZhOY4.js +28 -0
- package/dist/admin/index-CMAfPTry.js +121 -0
- package/dist/admin/index-hpPTiq45.mjs +122 -0
- package/dist/admin/index.js +4 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/admin/src/components/Initializer.d.ts +5 -0
- package/dist/admin/src/components/KontainerMediaInput.d.ts +3 -0
- package/dist/admin/src/components/PluginIcon.d.ts +2 -0
- package/dist/admin/src/index.d.ts +3 -0
- package/dist/admin/src/pages/Settings.d.ts +3 -0
- package/dist/admin/src/pluginId.d.ts +1 -0
- package/dist/admin/src/utils/getTranslation.d.ts +2 -0
- package/dist/server/index.js +356 -0
- package/dist/server/index.mjs +356 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +9 -0
- package/dist/server/src/content-types/index.d.ts +2 -0
- package/dist/server/src/controllers/controller.d.ts +11 -0
- package/dist/server/src/controllers/index.d.ts +12 -0
- package/dist/server/src/destroy.d.ts +5 -0
- package/dist/server/src/index.d.ts +108 -0
- package/dist/server/src/middlewares/index.d.ts +2 -0
- package/dist/server/src/policies/index.d.ts +2 -0
- package/dist/server/src/register.d.ts +5 -0
- package/dist/server/src/routes/admin/index.d.ts +12 -0
- package/dist/server/src/routes/content-api/index.d.ts +12 -0
- package/dist/server/src/routes/index.d.ts +25 -0
- package/dist/server/src/services/index.d.ts +51 -0
- package/dist/server/src/services/service.d.ts +57 -0
- 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;
|