strapi-plugin-document-metadata 1.0.1
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 +19 -0
- package/README.md +67 -0
- package/dist/_chunks/en-h9IAaoUJ.mjs +16 -0
- package/dist/_chunks/en-tDvQkVOI.js +16 -0
- package/dist/admin/index.js +311 -0
- package/dist/admin/index.mjs +312 -0
- package/dist/admin/src/components/DocumentMetadataCard/index.d.ts +10 -0
- package/dist/admin/src/components/DocumentMetadataGuard/index.d.ts +6 -0
- package/dist/admin/src/components/Initializer/index.d.ts +5 -0
- package/dist/admin/src/components/LastOpenedMetadataGuard/index.d.ts +10 -0
- package/dist/admin/src/components/LastOpenedMetadataLoader/index.d.ts +10 -0
- package/dist/admin/src/components/LastOpenedMetadataLoader/useLastOpened.d.ts +31 -0
- package/dist/admin/src/components/MetadataRow/index.d.ts +9 -0
- package/dist/admin/src/index.d.ts +10 -0
- package/dist/admin/src/pluginId.d.ts +1 -0
- package/dist/admin/src/utils/prefixKey.d.ts +2 -0
- package/dist/admin/src/utils/recentTimeFormatter.d.ts +16 -0
- package/dist/admin/src/utils/relativeDateFormatter.d.ts +30 -0
- package/dist/server/index.js +119 -0
- package/dist/server/index.mjs +120 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +5 -0
- package/dist/server/src/content-types/index.d.ts +2 -0
- package/dist/server/src/controllers/controller.d.ts +12 -0
- package/dist/server/src/controllers/index.d.ts +9 -0
- package/dist/server/src/destroy.d.ts +5 -0
- package/dist/server/src/index.d.ts +58 -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.d.ts +12 -0
- package/dist/server/src/routes/index.d.ts +14 -0
- package/dist/server/src/services/index.d.ts +19 -0
- package/dist/server/src/services/service.d.ts +35 -0
- package/package.json +78 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { useRef, useEffect, useState } from "react";
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useLocation } from "react-router-dom";
|
|
4
|
+
import { useFetchClient, useQueryParams, unstable_useDocument } from "@strapi/strapi/admin";
|
|
5
|
+
import { Grid, Typography, Box, Flex, Divider } from "@strapi/design-system";
|
|
6
|
+
import { Paperclip } from "@strapi/icons";
|
|
7
|
+
import { useIntl, FormattedMessage } from "react-intl";
|
|
8
|
+
const __variableDynamicImportRuntimeHelper = (glob, path, segs) => {
|
|
9
|
+
const v = glob[path];
|
|
10
|
+
if (v) {
|
|
11
|
+
return typeof v === "function" ? v() : Promise.resolve(v);
|
|
12
|
+
}
|
|
13
|
+
return new Promise((_, reject) => {
|
|
14
|
+
(typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)(
|
|
15
|
+
reject.bind(
|
|
16
|
+
null,
|
|
17
|
+
new Error(
|
|
18
|
+
"Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "")
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const PLUGIN_ID = "document-metadata";
|
|
25
|
+
const Initializer = ({ setPlugin }) => {
|
|
26
|
+
const ref = useRef(setPlugin);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
ref.current(PLUGIN_ID);
|
|
29
|
+
}, []);
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
const prefixKey = (key) => `${PLUGIN_ID}.${key}`;
|
|
33
|
+
const relativeDateFormatter = (date, textBuilder) => {
|
|
34
|
+
const today = /* @__PURE__ */ new Date();
|
|
35
|
+
if (today.toDateString() === date.toDateString()) {
|
|
36
|
+
return textBuilder.today(date.toLocaleTimeString());
|
|
37
|
+
}
|
|
38
|
+
const yesterday = /* @__PURE__ */ new Date();
|
|
39
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
40
|
+
if (yesterday.toDateString() === date.toDateString()) {
|
|
41
|
+
return textBuilder.yesterday(date.toLocaleTimeString());
|
|
42
|
+
}
|
|
43
|
+
return textBuilder.other(date.toLocaleString());
|
|
44
|
+
};
|
|
45
|
+
const config$3 = {
|
|
46
|
+
/** The threshold in minutes to determine if a date is considered "recent". */
|
|
47
|
+
thresholdInMinutes: 60
|
|
48
|
+
};
|
|
49
|
+
const recentTimeFormatter = ({
|
|
50
|
+
date,
|
|
51
|
+
fallbackFormatter
|
|
52
|
+
}) => {
|
|
53
|
+
const now = /* @__PURE__ */ new Date();
|
|
54
|
+
const minutesElapsedSinceNow = minutesElapsed(now, date);
|
|
55
|
+
if (minutesElapsedSinceNow >= config$3.thresholdInMinutes) {
|
|
56
|
+
return fallbackFormatter(date);
|
|
57
|
+
}
|
|
58
|
+
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
|
59
|
+
return rtf.format(-minutesElapsedSinceNow, "minutes");
|
|
60
|
+
};
|
|
61
|
+
const minutesElapsed = (now, then) => {
|
|
62
|
+
const millisecondsPerMinute = 60 * 1e3;
|
|
63
|
+
return Math.floor((now.getTime() - then.getTime()) / millisecondsPerMinute);
|
|
64
|
+
};
|
|
65
|
+
const MetadataRow = ({ title, line1, line2 }) => {
|
|
66
|
+
return /* @__PURE__ */ jsxs(Grid.Item, { direction: "column", alignItems: "stretch", children: [
|
|
67
|
+
/* @__PURE__ */ jsx(Typography, { variant: "sigma", children: title }),
|
|
68
|
+
/* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
|
|
69
|
+
line1,
|
|
70
|
+
/* @__PURE__ */ jsx("br", {}),
|
|
71
|
+
line2
|
|
72
|
+
] })
|
|
73
|
+
] });
|
|
74
|
+
};
|
|
75
|
+
var FetchStatus = /* @__PURE__ */ ((FetchStatus2) => {
|
|
76
|
+
FetchStatus2["Initial"] = "initial";
|
|
77
|
+
FetchStatus2["InProgress"] = "in-progress";
|
|
78
|
+
FetchStatus2["Success"] = "success";
|
|
79
|
+
FetchStatus2["Failure"] = "failure";
|
|
80
|
+
return FetchStatus2;
|
|
81
|
+
})(FetchStatus || {});
|
|
82
|
+
const config$2 = {
|
|
83
|
+
/** The configuration for the last-opened request. */
|
|
84
|
+
lastOpenedRequest: {
|
|
85
|
+
/** The path to fetch the last-opened metadata for the entity with the given `uid` and `documentId`. */
|
|
86
|
+
path: (uid, documentId) => `/document-metadata/last-opened/${uid}/${documentId}`
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const useLastOpened = ({
|
|
90
|
+
uid,
|
|
91
|
+
documentId,
|
|
92
|
+
locale
|
|
93
|
+
}) => {
|
|
94
|
+
const fetchClient = useFetchClient();
|
|
95
|
+
const [lastOpenedFetchState, setLastOpenedFetchState] = useState({
|
|
96
|
+
status: "initial"
|
|
97
|
+
/* Initial */
|
|
98
|
+
});
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const fetchLastOpened = async () => {
|
|
101
|
+
setLastOpenedFetchState({
|
|
102
|
+
status: "in-progress"
|
|
103
|
+
/* InProgress */
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
const { data: lastOpened } = await fetchClient.get(
|
|
107
|
+
config$2.lastOpenedRequest.path(uid, documentId),
|
|
108
|
+
{
|
|
109
|
+
params: { locale }
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
setLastOpenedFetchState({ status: "success", lastOpened });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(`Failed to fetch last-opened metadata: ${error}`);
|
|
115
|
+
setLastOpenedFetchState({ status: "failure", error });
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
fetchLastOpened();
|
|
119
|
+
}, [uid, documentId, locale]);
|
|
120
|
+
return lastOpenedFetchState;
|
|
121
|
+
};
|
|
122
|
+
const config$1 = {
|
|
123
|
+
/** The loading text to show while fetching data. */
|
|
124
|
+
loadingString: "…",
|
|
125
|
+
/** The non-breaking space character. */
|
|
126
|
+
nonBreakingSpace: " "
|
|
127
|
+
};
|
|
128
|
+
const LastOpenedMetadataLoader = ({
|
|
129
|
+
uid,
|
|
130
|
+
documentId,
|
|
131
|
+
locale
|
|
132
|
+
}) => {
|
|
133
|
+
const { formatMessage } = useIntl();
|
|
134
|
+
const translate = (key, values) => formatMessage({ id: prefixKey(key) }, values);
|
|
135
|
+
const lastOpenedFetchState = useLastOpened({ uid, documentId, locale });
|
|
136
|
+
switch (lastOpenedFetchState.status) {
|
|
137
|
+
case FetchStatus.Initial:
|
|
138
|
+
case FetchStatus.InProgress:
|
|
139
|
+
return /* @__PURE__ */ jsx(
|
|
140
|
+
MetadataRow,
|
|
141
|
+
{
|
|
142
|
+
title: translate("opened-at"),
|
|
143
|
+
line1: config$1.loadingString,
|
|
144
|
+
line2: config$1.nonBreakingSpace
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
case FetchStatus.Failure:
|
|
148
|
+
return null;
|
|
149
|
+
case FetchStatus.Success:
|
|
150
|
+
const lastOpened = lastOpenedFetchState.lastOpened;
|
|
151
|
+
if (!lastOpened.openedAt || !lastOpened.openedBy) {
|
|
152
|
+
return /* @__PURE__ */ jsx(MetadataRow, { title: translate("opened-at"), line1: translate("opened-first-time") });
|
|
153
|
+
}
|
|
154
|
+
const formattedOpenedAt = recentTimeFormatter({
|
|
155
|
+
date: new Date(lastOpened.openedAt),
|
|
156
|
+
fallbackFormatter: (date) => relativeDateFormatter(date, {
|
|
157
|
+
today: (formattedTime) => translate("date.today", { formattedTime }),
|
|
158
|
+
yesterday: (formattedTime) => translate("date.yesterday", { formattedTime }),
|
|
159
|
+
other: (formattedDate) => translate("date.other", { formattedDate })
|
|
160
|
+
})
|
|
161
|
+
});
|
|
162
|
+
const formattedOpenedBy = translate("opened-by", { username: lastOpened.openedBy });
|
|
163
|
+
return /* @__PURE__ */ jsx(
|
|
164
|
+
MetadataRow,
|
|
165
|
+
{
|
|
166
|
+
title: translate("opened-at"),
|
|
167
|
+
line1: formattedOpenedAt,
|
|
168
|
+
line2: formattedOpenedBy
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const LastOpenedMetadataGuard = ({
|
|
174
|
+
uid,
|
|
175
|
+
document
|
|
176
|
+
}) => {
|
|
177
|
+
const hasLastOpenedFields = "openedAt" in document && "openedBy" in document;
|
|
178
|
+
if (!hasLastOpenedFields) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return /* @__PURE__ */ jsx(LastOpenedMetadataLoader, { uid, documentId: document.documentId, locale: document.locale });
|
|
182
|
+
};
|
|
183
|
+
const DocumentMetadataCard = ({
|
|
184
|
+
collectionType,
|
|
185
|
+
uid,
|
|
186
|
+
documentId
|
|
187
|
+
}) => {
|
|
188
|
+
const { formatMessage } = useIntl();
|
|
189
|
+
const translate = (key, values) => formatMessage({ id: prefixKey(key) }, values);
|
|
190
|
+
const initialParams = { plugins: { i18n: { locale: void 0 } } };
|
|
191
|
+
const [queryParams, _] = useQueryParams(initialParams);
|
|
192
|
+
const locale = queryParams.query.plugins.i18n.locale;
|
|
193
|
+
const { document } = unstable_useDocument({ documentId, model: uid, collectionType, params: { locale } });
|
|
194
|
+
if (!document) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const formatDate = (date) => recentTimeFormatter({
|
|
198
|
+
date: new Date(date),
|
|
199
|
+
fallbackFormatter: (date2) => relativeDateFormatter(date2, {
|
|
200
|
+
today: (formattedTime) => translate("date.today", { formattedTime }),
|
|
201
|
+
yesterday: (formattedTime) => translate("date.yesterday", { formattedTime }),
|
|
202
|
+
other: (formattedDate) => translate("date.other", { formattedDate })
|
|
203
|
+
})
|
|
204
|
+
});
|
|
205
|
+
const formatUsername = (user) => `${user.firstname} ${user.lastname}`;
|
|
206
|
+
let formattedUpdatedAt = formatDate(new Date(document.updatedAt));
|
|
207
|
+
let formattedUpdatedBy = document.updatedBy ? translate("updated-by", { username: formatUsername(document.updatedBy) }) : "";
|
|
208
|
+
let formattedCreatedAt = formatDate(new Date(document.createdAt));
|
|
209
|
+
let formattedCreatedBy = document.createdBy ? translate("created-by", { username: formatUsername(document.createdBy) }) : "";
|
|
210
|
+
return /* @__PURE__ */ jsx(
|
|
211
|
+
Box,
|
|
212
|
+
{
|
|
213
|
+
width: "100%",
|
|
214
|
+
padding: "16px",
|
|
215
|
+
background: "neutral0",
|
|
216
|
+
hasRadius: true,
|
|
217
|
+
borderColor: "neutral200",
|
|
218
|
+
borderStyle: "solid",
|
|
219
|
+
borderWidth: "1px",
|
|
220
|
+
children: /* @__PURE__ */ jsxs(Grid.Root, { gap: "8px", gridCols: 1, children: [
|
|
221
|
+
/* @__PURE__ */ jsxs(Grid.Item, { direction: "column", alignItems: "stretch", children: [
|
|
222
|
+
/* @__PURE__ */ jsxs(Flex, { gap: "8px", direction: "row", alignItems: "center", justifyContent: "space-between", children: [
|
|
223
|
+
/* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: /* @__PURE__ */ jsx(FormattedMessage, { id: prefixKey("title") }) }),
|
|
224
|
+
/* @__PURE__ */ jsx(Paperclip, { fill: "neutral600", style: { marginRight: "4px" } })
|
|
225
|
+
] }),
|
|
226
|
+
/* @__PURE__ */ jsx(Divider, { style: { marginTop: "6px", marginBottom: "4px" } })
|
|
227
|
+
] }),
|
|
228
|
+
/* @__PURE__ */ jsx(LastOpenedMetadataGuard, { uid, document }),
|
|
229
|
+
/* @__PURE__ */ jsx(
|
|
230
|
+
MetadataRow,
|
|
231
|
+
{
|
|
232
|
+
title: translate("updated-at"),
|
|
233
|
+
line1: formattedUpdatedAt,
|
|
234
|
+
line2: formattedUpdatedBy
|
|
235
|
+
}
|
|
236
|
+
),
|
|
237
|
+
/* @__PURE__ */ jsx(
|
|
238
|
+
MetadataRow,
|
|
239
|
+
{
|
|
240
|
+
title: translate("created-at"),
|
|
241
|
+
line1: formattedCreatedAt,
|
|
242
|
+
line2: formattedCreatedBy
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
] })
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
const config = {
|
|
250
|
+
/**
|
|
251
|
+
* The supported collection type.
|
|
252
|
+
* We currently only support "collection-types", as "single-types" don't have their document ID as part of the URL.
|
|
253
|
+
*/
|
|
254
|
+
supportedCollectionType: "collection-types",
|
|
255
|
+
/**
|
|
256
|
+
* The expected length of a valid document ID.
|
|
257
|
+
*
|
|
258
|
+
* > To address this limitation, Strapi 5 introduced documentId, a 24-character alphanumeric string, as a unique and
|
|
259
|
+
* > persistent identifier for a content entry, independent of its physical records.
|
|
260
|
+
*
|
|
261
|
+
* https://docs.strapi.io/cms/api/document-service
|
|
262
|
+
*/
|
|
263
|
+
documentIdLength: 24
|
|
264
|
+
};
|
|
265
|
+
const DocumentMetadataGuard = () => {
|
|
266
|
+
const location = useLocation();
|
|
267
|
+
const urlPathComponents = location.pathname.split("/").filter(Boolean);
|
|
268
|
+
const [collectionType, uid, documentId] = urlPathComponents.slice(-3);
|
|
269
|
+
const isValidCollectionType = collectionType === config.supportedCollectionType;
|
|
270
|
+
const isValidUID = uid && uid.length;
|
|
271
|
+
const isValidDocumentId = documentId && documentId.length === config.documentIdLength;
|
|
272
|
+
if (!isValidCollectionType || !isValidUID || !isValidDocumentId) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return /* @__PURE__ */ jsx(
|
|
276
|
+
DocumentMetadataCard,
|
|
277
|
+
{
|
|
278
|
+
collectionType,
|
|
279
|
+
uid,
|
|
280
|
+
documentId
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
const index = {
|
|
285
|
+
register(app) {
|
|
286
|
+
app.getPlugin("content-manager").injectComponent("editView", "right-links", {
|
|
287
|
+
name: "DocumentMetadataGuard",
|
|
288
|
+
Component: DocumentMetadataGuard
|
|
289
|
+
});
|
|
290
|
+
app.registerPlugin({
|
|
291
|
+
id: PLUGIN_ID,
|
|
292
|
+
initializer: Initializer,
|
|
293
|
+
isReady: false,
|
|
294
|
+
name: PLUGIN_ID
|
|
295
|
+
});
|
|
296
|
+
},
|
|
297
|
+
async registerTrads({ locales }) {
|
|
298
|
+
return Promise.all(
|
|
299
|
+
locales.map(async (locale) => {
|
|
300
|
+
try {
|
|
301
|
+
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("../_chunks/en-h9IAaoUJ.mjs") }), `./translations/${locale}.json`, 3);
|
|
302
|
+
return { data, locale };
|
|
303
|
+
} catch {
|
|
304
|
+
return { data: {}, locale };
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
export {
|
|
311
|
+
index as default
|
|
312
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CollectionType, ContentTypeUID, DocumentID } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* Renders a document metadata card showing various metadata about the given document.
|
|
4
|
+
*/
|
|
5
|
+
declare const DocumentMetadataCard: ({ collectionType, uid, documentId, }: {
|
|
6
|
+
collectionType: CollectionType;
|
|
7
|
+
uid: ContentTypeUID;
|
|
8
|
+
documentId: DocumentID;
|
|
9
|
+
}) => import("react/jsx-runtime").JSX.Element | null;
|
|
10
|
+
export default DocumentMetadataCard;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A wrapper component that conditionally renders the `DocumentMetadataCard` and its contents
|
|
3
|
+
* when the current document being edited is supported.
|
|
4
|
+
*/
|
|
5
|
+
declare const DocumentMetadataGuard: () => import("react/jsx-runtime").JSX.Element | null;
|
|
6
|
+
export default DocumentMetadataGuard;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AnyDocument, ContentTypeUID } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* A wrapper component that conditionally renders the `LastOpenedMetadataLoader`
|
|
4
|
+
* when the current content-type being edited contains the last-opened fields.
|
|
5
|
+
*/
|
|
6
|
+
declare const LastOpenedMetadataGuard: ({ uid, document, }: {
|
|
7
|
+
uid: ContentTypeUID;
|
|
8
|
+
document: AnyDocument;
|
|
9
|
+
}) => import("react/jsx-runtime").JSX.Element | null;
|
|
10
|
+
export default LastOpenedMetadataGuard;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ContentTypeUID, DocumentID } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* Fetches the last-opened metadata for the given document.
|
|
4
|
+
*/
|
|
5
|
+
declare const LastOpenedMetadataLoader: ({ uid, documentId, locale, }: {
|
|
6
|
+
uid: ContentTypeUID;
|
|
7
|
+
documentId: DocumentID;
|
|
8
|
+
locale: string | undefined;
|
|
9
|
+
}) => import("react/jsx-runtime").JSX.Element | null;
|
|
10
|
+
export default LastOpenedMetadataLoader;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ContentTypeUID, DocumentID, LastOpened } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* Represents the different states of a fetch workflow.
|
|
4
|
+
*/
|
|
5
|
+
export declare enum FetchStatus {
|
|
6
|
+
Initial = "initial",
|
|
7
|
+
InProgress = "in-progress",
|
|
8
|
+
Success = "success",
|
|
9
|
+
Failure = "failure"
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A discriminated union describing the current state of last-opened fetch process.
|
|
13
|
+
* Each state can optionally include payload data relevant to that stage.
|
|
14
|
+
*/
|
|
15
|
+
type LastOpenedFetchState = {
|
|
16
|
+
status: FetchStatus.Initial;
|
|
17
|
+
} | {
|
|
18
|
+
status: FetchStatus.InProgress;
|
|
19
|
+
} | {
|
|
20
|
+
status: FetchStatus.Success;
|
|
21
|
+
lastOpened: LastOpened;
|
|
22
|
+
} | {
|
|
23
|
+
status: FetchStatus.Failure;
|
|
24
|
+
error: any;
|
|
25
|
+
};
|
|
26
|
+
export declare const useLastOpened: ({ uid, documentId, locale, }: {
|
|
27
|
+
uid: ContentTypeUID;
|
|
28
|
+
documentId: DocumentID;
|
|
29
|
+
locale: string | undefined;
|
|
30
|
+
}) => LastOpenedFetchState;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Displays a single metadata row consisting of a title and one or two lines of descriptive text.
|
|
3
|
+
*/
|
|
4
|
+
declare const MetadataRow: ({ title, line1, line2 }: {
|
|
5
|
+
title: string;
|
|
6
|
+
line1: string;
|
|
7
|
+
line2?: string;
|
|
8
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export default MetadataRow;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PLUGIN_ID = "document-metadata";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a given `Date` object into a human-readable string for **recent dates**,
|
|
3
|
+
* and delegates to a fallback formatter for older dates.
|
|
4
|
+
*
|
|
5
|
+
* - If the date is within the **last 60 minutes**, it returns a relative time string like `"5 minutes ago"`.
|
|
6
|
+
* - Otherwise, it falls back to the `relativeDateFormatter()` function for formatting.
|
|
7
|
+
*
|
|
8
|
+
* @param params.date - The `Date` object to format.
|
|
9
|
+
* @param params.fallbackFormatter - A function that formats dates which are not considered "recent".
|
|
10
|
+
*
|
|
11
|
+
* @returns A human-readable relative date string based on the provided date.
|
|
12
|
+
*/
|
|
13
|
+
export declare const recentTimeFormatter: ({ date, fallbackFormatter, }: {
|
|
14
|
+
date: Date;
|
|
15
|
+
fallbackFormatter: (date: Date) => string;
|
|
16
|
+
}) => string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines a set of functions that build human-readable text for relative dates.
|
|
3
|
+
*
|
|
4
|
+
* This interface allows customization of how "today", "yesterday",
|
|
5
|
+
* and "other" dates are represented in formatted output.
|
|
6
|
+
*
|
|
7
|
+
* Example:
|
|
8
|
+
* ```ts
|
|
9
|
+
* const textBuilder: RelativeDateTextBuilder = {
|
|
10
|
+
* today: (time) => `Today at ${time}`,
|
|
11
|
+
* yesterday: (time) => `Yesterday at ${time}`,
|
|
12
|
+
* other: (date) => date,
|
|
13
|
+
* };
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
interface RelativeDateTextBuilder {
|
|
17
|
+
today: (formattedTime: string) => string;
|
|
18
|
+
yesterday: (formattedTime: string) => string;
|
|
19
|
+
other: (formattedDate: string) => string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Formats a given `Date` object into a human-readable, relative date string.
|
|
23
|
+
*
|
|
24
|
+
* @param date - The `Date` object to format.
|
|
25
|
+
* @param textBuilder - A builder object that defines how to format "today", "yesterday", and other dates.
|
|
26
|
+
*
|
|
27
|
+
* @returns A human-readable relative date string based on the provided date.
|
|
28
|
+
*/
|
|
29
|
+
export declare const relativeDateFormatter: (date: Date, textBuilder: RelativeDateTextBuilder) => string;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const bootstrap = ({ strapi }) => {
|
|
3
|
+
};
|
|
4
|
+
const destroy = ({ strapi }) => {
|
|
5
|
+
};
|
|
6
|
+
const register = ({ strapi }) => {
|
|
7
|
+
};
|
|
8
|
+
const config = {
|
|
9
|
+
default: {},
|
|
10
|
+
validator() {
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const contentTypes = {};
|
|
14
|
+
const controller = ({ strapi }) => ({
|
|
15
|
+
/**
|
|
16
|
+
* Controller method for the route that fetches and updates the last-opened fields
|
|
17
|
+
* of a document with the given `uid` and `documentId` path parameters (GET request).
|
|
18
|
+
*/
|
|
19
|
+
async lastOpened(ctx) {
|
|
20
|
+
const { uid, documentId } = ctx.params;
|
|
21
|
+
const { locale } = ctx.request.query;
|
|
22
|
+
const previousLastOpened = await strapi.plugin("document-metadata").service("service").fetchLastOpened({ uid, documentId, locale });
|
|
23
|
+
const openedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
24
|
+
const { user } = ctx.state;
|
|
25
|
+
const openedBy = `${user.firstname} ${user.lastname}`;
|
|
26
|
+
await strapi.plugin("document-metadata").service("service").updateLastOpened({ uid, documentId, locale, openedAt, openedBy });
|
|
27
|
+
ctx.response.body = previousLastOpened;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
const controllers = {
|
|
31
|
+
controller
|
|
32
|
+
};
|
|
33
|
+
const middlewares = {};
|
|
34
|
+
const policies = {};
|
|
35
|
+
const admin = {
|
|
36
|
+
type: "admin",
|
|
37
|
+
routes: [
|
|
38
|
+
{
|
|
39
|
+
method: "GET",
|
|
40
|
+
path: "/last-opened/:uid/:documentId",
|
|
41
|
+
handler: "controller.lastOpened",
|
|
42
|
+
config: {
|
|
43
|
+
policies: []
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
};
|
|
48
|
+
const routes = {
|
|
49
|
+
admin
|
|
50
|
+
};
|
|
51
|
+
const service = ({ strapi }) => ({
|
|
52
|
+
/**
|
|
53
|
+
* Fetches the last-opened fields for a specific document within a content type.
|
|
54
|
+
*
|
|
55
|
+
* @param uid - The unique identifier of the content type (e.g. 'api::products.products').
|
|
56
|
+
* @param documentId - The ID of the document to fetch.
|
|
57
|
+
* @param locale - The current locale of the content type / `undefined` if localization is turned off.
|
|
58
|
+
*/
|
|
59
|
+
fetchLastOpened: async ({
|
|
60
|
+
uid,
|
|
61
|
+
documentId,
|
|
62
|
+
locale
|
|
63
|
+
}) => {
|
|
64
|
+
return await strapi.documents(uid).findOne({
|
|
65
|
+
documentId,
|
|
66
|
+
fields: ["openedAt", "openedBy"],
|
|
67
|
+
locale
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* Updates the last-opened fields for a specific document within a content type.
|
|
72
|
+
*
|
|
73
|
+
* @param uid - The unique identifier of the content type (e.g. 'api::products.products').
|
|
74
|
+
* @param documentId - The ID of the document to update.
|
|
75
|
+
* @param locale - The current locale of the content type / `undefined` if localization is turned off.
|
|
76
|
+
* @param openedAt - The date and time when the document was last opened.
|
|
77
|
+
* @param openedBy - The name of the user who last opened the document.
|
|
78
|
+
*/
|
|
79
|
+
async updateLastOpened({
|
|
80
|
+
uid,
|
|
81
|
+
documentId,
|
|
82
|
+
locale,
|
|
83
|
+
openedAt,
|
|
84
|
+
openedBy
|
|
85
|
+
}) {
|
|
86
|
+
const tableName = strapi.getModel(uid).collectionName;
|
|
87
|
+
if (!tableName) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Expected to have a collection name for the content type "${uid}" at this point.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return await strapi.db.connection(tableName).update({
|
|
93
|
+
opened_at: openedAt,
|
|
94
|
+
opened_by: openedBy
|
|
95
|
+
}).where({
|
|
96
|
+
document_id: documentId,
|
|
97
|
+
// We explicitly need to provide `null` here, cause in the database
|
|
98
|
+
// the locale is stored as `NULL` when localization is turned off.
|
|
99
|
+
// Without this fallback, the query would not match any rows.
|
|
100
|
+
locale: locale || null
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const services = {
|
|
105
|
+
service
|
|
106
|
+
};
|
|
107
|
+
const index = {
|
|
108
|
+
register,
|
|
109
|
+
bootstrap,
|
|
110
|
+
destroy,
|
|
111
|
+
config,
|
|
112
|
+
controllers,
|
|
113
|
+
routes,
|
|
114
|
+
services,
|
|
115
|
+
contentTypes,
|
|
116
|
+
policies,
|
|
117
|
+
middlewares
|
|
118
|
+
};
|
|
119
|
+
module.exports = index;
|