@strapi/content-releases 0.0.0-next.504ae21185714e6995d2bdd6458efe2e20371a84 → 0.0.0-next.583e758623dc82206a4b2758d01dd5948b6e3f6a

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 (29) hide show
  1. package/dist/_chunks/{App-3ycH2d3s.mjs → App-6ugQxqYE.mjs} +699 -440
  2. package/dist/_chunks/App-6ugQxqYE.mjs.map +1 -0
  3. package/dist/_chunks/{App-5PsAyVt2.js → App-P1kyM3gT.js} +691 -431
  4. package/dist/_chunks/App-P1kyM3gT.js.map +1 -0
  5. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs +51 -0
  6. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +1 -0
  7. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js +51 -0
  8. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +1 -0
  9. package/dist/_chunks/{en-SOqjCdyh.mjs → en-WuuhP6Bn.mjs} +18 -6
  10. package/dist/_chunks/en-WuuhP6Bn.mjs.map +1 -0
  11. package/dist/_chunks/{en-2DuPv5k0.js → en-gcJJ5htG.js} +18 -6
  12. package/dist/_chunks/en-gcJJ5htG.js.map +1 -0
  13. package/dist/_chunks/{index-D57Rztnc.js → index-2xzbhaQP.js} +127 -23
  14. package/dist/_chunks/index-2xzbhaQP.js.map +1 -0
  15. package/dist/_chunks/{index-4gUWuCQV.mjs → index-_eBuegHN.mjs} +138 -34
  16. package/dist/_chunks/index-_eBuegHN.mjs.map +1 -0
  17. package/dist/admin/index.js +1 -1
  18. package/dist/admin/index.mjs +2 -2
  19. package/dist/server/index.js +863 -419
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +862 -419
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +14 -11
  24. package/dist/_chunks/App-3ycH2d3s.mjs.map +0 -1
  25. package/dist/_chunks/App-5PsAyVt2.js.map +0 -1
  26. package/dist/_chunks/en-2DuPv5k0.js.map +0 -1
  27. package/dist/_chunks/en-SOqjCdyh.mjs.map +0 -1
  28. package/dist/_chunks/index-4gUWuCQV.mjs.map +0 -1
  29. package/dist/_chunks/index-D57Rztnc.js.map +0 -1
@@ -3,14 +3,17 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const jsxRuntime = require("react/jsx-runtime");
4
4
  const helperPlugin = require("@strapi/helper-plugin");
5
5
  const reactRouterDom = require("react-router-dom");
6
- const index = require("./index-D57Rztnc.js");
6
+ const index = require("./index-2xzbhaQP.js");
7
7
  const React = require("react");
8
8
  const strapiAdmin = require("@strapi/admin/strapi-admin");
9
9
  const designSystem = require("@strapi/design-system");
10
10
  const v2 = require("@strapi/design-system/v2");
11
11
  const icons = require("@strapi/icons");
12
+ const format = require("date-fns/format");
13
+ const dateFnsTz = require("date-fns-tz");
12
14
  const reactIntl = require("react-intl");
13
15
  const styled = require("styled-components");
16
+ const dateFns = require("date-fns");
14
17
  const formik = require("formik");
15
18
  const yup = require("yup");
16
19
  require("@reduxjs/toolkit/query");
@@ -37,10 +40,28 @@ function _interopNamespace(e) {
37
40
  return Object.freeze(n);
38
41
  }
39
42
  const React__namespace = /* @__PURE__ */ _interopNamespace(React);
43
+ const format__default = /* @__PURE__ */ _interopDefault(format);
40
44
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
41
45
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
42
46
  const RELEASE_SCHEMA = yup__namespace.object().shape({
43
- name: yup__namespace.string().trim().required()
47
+ name: yup__namespace.string().trim().required(),
48
+ scheduledAt: yup__namespace.string().nullable(),
49
+ isScheduled: yup__namespace.boolean().optional(),
50
+ time: yup__namespace.string().when("isScheduled", {
51
+ is: true,
52
+ then: yup__namespace.string().trim().required(),
53
+ otherwise: yup__namespace.string().nullable()
54
+ }),
55
+ timezone: yup__namespace.string().when("isScheduled", {
56
+ is: true,
57
+ then: yup__namespace.string().required().nullable(),
58
+ otherwise: yup__namespace.string().nullable()
59
+ }),
60
+ date: yup__namespace.string().when("isScheduled", {
61
+ is: true,
62
+ then: yup__namespace.string().required().nullable(),
63
+ otherwise: yup__namespace.string().nullable()
64
+ })
44
65
  }).required().noUnknown();
45
66
  const ReleaseModal = ({
46
67
  handleClose,
@@ -51,6 +72,24 @@ const ReleaseModal = ({
51
72
  const { formatMessage } = reactIntl.useIntl();
52
73
  const { pathname } = reactRouterDom.useLocation();
53
74
  const isCreatingRelease = pathname === `/plugins/${index.pluginId}`;
75
+ const IsSchedulingEnabled = window.strapi.future.isEnabled("contentReleasesScheduling");
76
+ const { timezoneList, systemTimezone = { value: "UTC+00:00-Africa/Abidjan " } } = getTimezones(
77
+ initialValues.scheduledAt ? new Date(initialValues.scheduledAt) : /* @__PURE__ */ new Date()
78
+ );
79
+ const getScheduledTimestamp = (values) => {
80
+ const { date, time, timezone } = values;
81
+ if (!date || !time || !timezone)
82
+ return null;
83
+ const formattedDate = dateFns.parse(time, "HH:mm", new Date(date));
84
+ const timezoneWithoutOffset = timezone.split("_")[1];
85
+ return dateFnsTz.zonedTimeToUtc(formattedDate, timezoneWithoutOffset);
86
+ };
87
+ const getTimezoneWithOffset = () => {
88
+ const currentTimezone = timezoneList.find(
89
+ (timezone) => timezone.value.split("_")[1] === initialValues.timezone
90
+ );
91
+ return currentTimezone?.value || systemTimezone.value;
92
+ };
54
93
  return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.ModalLayout, { onClose: handleClose, labelledBy: "title", children: [
55
94
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.ModalHeader, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { id: "title", fontWeight: "bold", textColor: "neutral800", children: formatMessage(
56
95
  {
@@ -62,45 +101,134 @@ const ReleaseModal = ({
62
101
  /* @__PURE__ */ jsxRuntime.jsx(
63
102
  formik.Formik,
64
103
  {
65
- validateOnChange: false,
66
- onSubmit: handleSubmit,
67
- initialValues,
104
+ onSubmit: (values) => {
105
+ handleSubmit({
106
+ ...values,
107
+ timezone: values.timezone ? values.timezone.split("_")[1] : null,
108
+ scheduledAt: values.isScheduled ? getScheduledTimestamp(values) : null
109
+ });
110
+ },
111
+ initialValues: {
112
+ ...initialValues,
113
+ timezone: initialValues.timezone ? getTimezoneWithOffset() : systemTimezone.value
114
+ },
68
115
  validationSchema: RELEASE_SCHEMA,
69
- children: ({ values, errors, handleChange }) => /* @__PURE__ */ jsxRuntime.jsxs(formik.Form, { children: [
70
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.ModalBody, { children: /* @__PURE__ */ jsxRuntime.jsx(
71
- designSystem.TextInput,
72
- {
73
- label: formatMessage({
74
- id: "content-releases.modal.form.input.label.release-name",
75
- defaultMessage: "Name"
76
- }),
77
- name: "name",
78
- value: values.name,
79
- error: errors.name,
80
- onChange: handleChange,
81
- required: true
82
- }
83
- ) }),
116
+ validateOnChange: false,
117
+ children: ({ values, errors, handleChange, setFieldValue }) => /* @__PURE__ */ jsxRuntime.jsxs(formik.Form, { children: [
118
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.ModalBody, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 6, children: [
119
+ /* @__PURE__ */ jsxRuntime.jsx(
120
+ designSystem.TextInput,
121
+ {
122
+ label: formatMessage({
123
+ id: "content-releases.modal.form.input.label.release-name",
124
+ defaultMessage: "Name"
125
+ }),
126
+ name: "name",
127
+ value: values.name,
128
+ error: errors.name,
129
+ onChange: handleChange,
130
+ required: true
131
+ }
132
+ ),
133
+ IsSchedulingEnabled && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
134
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { width: "max-content", children: /* @__PURE__ */ jsxRuntime.jsx(
135
+ designSystem.Checkbox,
136
+ {
137
+ name: "isScheduled",
138
+ value: values.isScheduled,
139
+ onChange: (event) => {
140
+ setFieldValue("isScheduled", event.target.checked);
141
+ if (!event.target.checked) {
142
+ setFieldValue("date", null);
143
+ setFieldValue("time", "");
144
+ setFieldValue("timezone", null);
145
+ } else {
146
+ setFieldValue("date", initialValues.date);
147
+ setFieldValue("time", initialValues.time);
148
+ setFieldValue(
149
+ "timezone",
150
+ initialValues.timezone ?? systemTimezone?.value
151
+ );
152
+ }
153
+ },
154
+ children: /* @__PURE__ */ jsxRuntime.jsx(
155
+ designSystem.Typography,
156
+ {
157
+ textColor: values.isScheduled ? "primary600" : "neutral800",
158
+ fontWeight: values.isScheduled ? "semiBold" : "regular",
159
+ children: formatMessage({
160
+ id: "modal.form.input.label.schedule-release",
161
+ defaultMessage: "Schedule release"
162
+ })
163
+ }
164
+ )
165
+ }
166
+ ) }),
167
+ values.isScheduled && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
168
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, alignItems: "start", children: [
169
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { width: "100%", children: /* @__PURE__ */ jsxRuntime.jsx(
170
+ designSystem.DatePicker,
171
+ {
172
+ label: formatMessage({
173
+ id: "content-releases.modal.form.input.label.date",
174
+ defaultMessage: "Date"
175
+ }),
176
+ name: "date",
177
+ error: errors.date,
178
+ onChange: (date) => {
179
+ const isoFormatDate = date ? dateFns.formatISO(date, { representation: "date" }) : null;
180
+ setFieldValue("date", isoFormatDate);
181
+ },
182
+ clearLabel: formatMessage({
183
+ id: "content-releases.modal.form.input.clearLabel",
184
+ defaultMessage: "Clear"
185
+ }),
186
+ onClear: () => {
187
+ setFieldValue("date", null);
188
+ },
189
+ selectedDate: values.date || void 0,
190
+ required: true
191
+ }
192
+ ) }),
193
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { width: "100%", children: /* @__PURE__ */ jsxRuntime.jsx(
194
+ designSystem.TimePicker,
195
+ {
196
+ label: formatMessage({
197
+ id: "content-releases.modal.form.input.label.time",
198
+ defaultMessage: "Time"
199
+ }),
200
+ name: "time",
201
+ error: errors.time,
202
+ onChange: (time) => {
203
+ setFieldValue("time", time);
204
+ },
205
+ clearLabel: formatMessage({
206
+ id: "content-releases.modal.form.input.clearLabel",
207
+ defaultMessage: "Clear"
208
+ }),
209
+ onClear: () => {
210
+ setFieldValue("time", "");
211
+ },
212
+ value: values.time || void 0,
213
+ required: true
214
+ }
215
+ ) })
216
+ ] }),
217
+ /* @__PURE__ */ jsxRuntime.jsx(TimezoneComponent, { timezoneOptions: timezoneList })
218
+ ] })
219
+ ] })
220
+ ] }) }),
84
221
  /* @__PURE__ */ jsxRuntime.jsx(
85
222
  designSystem.ModalFooter,
86
223
  {
87
224
  startActions: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: handleClose, variant: "tertiary", name: "cancel", children: formatMessage({ id: "cancel", defaultMessage: "Cancel" }) }),
88
- endActions: /* @__PURE__ */ jsxRuntime.jsx(
89
- designSystem.Button,
225
+ endActions: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { name: "submit", loading: isLoading, type: "submit", children: formatMessage(
90
226
  {
91
- name: "submit",
92
- loading: isLoading,
93
- disabled: !values.name || values.name === initialValues.name,
94
- type: "submit",
95
- children: formatMessage(
96
- {
97
- id: "content-releases.modal.form.button.submit",
98
- defaultMessage: "{isCreatingRelease, select, true {Continue} other {Save}}"
99
- },
100
- { isCreatingRelease }
101
- )
102
- }
103
- )
227
+ id: "content-releases.modal.form.button.submit",
228
+ defaultMessage: "{isCreatingRelease, select, true {Continue} other {Save}}"
229
+ },
230
+ { isCreatingRelease }
231
+ ) })
104
232
  }
105
233
  )
106
234
  ] })
@@ -108,33 +236,403 @@ const ReleaseModal = ({
108
236
  )
109
237
  ] });
110
238
  };
239
+ const getTimezones = (selectedDate) => {
240
+ const timezoneList = Intl.supportedValuesOf("timeZone").map((timezone) => {
241
+ const utcOffset = index.getTimezoneOffset(timezone, selectedDate);
242
+ return { offset: utcOffset, value: `${utcOffset}_${timezone}` };
243
+ });
244
+ const systemTimezone = timezoneList.find(
245
+ (timezone) => timezone.value.split("_")[1] === Intl.DateTimeFormat().resolvedOptions().timeZone
246
+ );
247
+ return { timezoneList, systemTimezone };
248
+ };
249
+ const TimezoneComponent = ({ timezoneOptions }) => {
250
+ const { values, errors, setFieldValue } = formik.useFormikContext();
251
+ const { formatMessage } = reactIntl.useIntl();
252
+ const [timezoneList, setTimezoneList] = React__namespace.useState(timezoneOptions);
253
+ React__namespace.useEffect(() => {
254
+ if (values.date) {
255
+ const { timezoneList: timezoneList2 } = getTimezones(new Date(values.date));
256
+ setTimezoneList(timezoneList2);
257
+ const updatedTimezone = values.timezone && timezoneList2.find((tz) => tz.value.split("_")[1] === values.timezone.split("_")[1]);
258
+ if (updatedTimezone) {
259
+ setFieldValue("timezone", updatedTimezone.value);
260
+ }
261
+ }
262
+ }, [setFieldValue, values.date, values.timezone]);
263
+ return /* @__PURE__ */ jsxRuntime.jsx(
264
+ designSystem.Combobox,
265
+ {
266
+ label: formatMessage({
267
+ id: "content-releases.modal.form.input.label.timezone",
268
+ defaultMessage: "Timezone"
269
+ }),
270
+ name: "timezone",
271
+ value: values.timezone || void 0,
272
+ textValue: values.timezone ? values.timezone.replace("_", " ") : void 0,
273
+ onChange: (timezone) => {
274
+ setFieldValue("timezone", timezone);
275
+ },
276
+ onTextValueChange: (timezone) => {
277
+ setFieldValue("timezone", timezone);
278
+ },
279
+ onClear: () => {
280
+ setFieldValue("timezone", "");
281
+ },
282
+ error: errors.timezone,
283
+ required: true,
284
+ children: timezoneList.map((timezone) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.ComboboxOption, { value: timezone.value, children: timezone.value.replace("_", " ") }, timezone.value))
285
+ }
286
+ );
287
+ };
288
+ const LinkCard = styled__default.default(v2.Link)`
289
+ display: block;
290
+ `;
291
+ const CapitalizeRelativeTime = styled__default.default(helperPlugin.RelativeTime)`
292
+ text-transform: capitalize;
293
+ `;
294
+ const getBadgeProps = (status) => {
295
+ let color;
296
+ switch (status) {
297
+ case "ready":
298
+ color = "success";
299
+ break;
300
+ case "blocked":
301
+ color = "warning";
302
+ break;
303
+ case "failed":
304
+ color = "danger";
305
+ break;
306
+ case "done":
307
+ color = "primary";
308
+ break;
309
+ case "empty":
310
+ default:
311
+ color = "neutral";
312
+ }
313
+ return {
314
+ textColor: `${color}600`,
315
+ backgroundColor: `${color}100`,
316
+ borderColor: `${color}200`
317
+ };
318
+ };
319
+ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }) => {
320
+ const { formatMessage } = reactIntl.useIntl();
321
+ const IsSchedulingEnabled = window.strapi.future.isEnabled("contentReleasesScheduling");
322
+ if (isError) {
323
+ return /* @__PURE__ */ jsxRuntime.jsx(helperPlugin.AnErrorOccurred, {});
324
+ }
325
+ if (releases?.length === 0) {
326
+ return /* @__PURE__ */ jsxRuntime.jsx(
327
+ designSystem.EmptyStateLayout,
328
+ {
329
+ content: formatMessage(
330
+ {
331
+ id: "content-releases.page.Releases.tab.emptyEntries",
332
+ defaultMessage: "No releases"
333
+ },
334
+ {
335
+ target: sectionTitle
336
+ }
337
+ ),
338
+ icon: /* @__PURE__ */ jsxRuntime.jsx(icons.EmptyDocuments, { width: "10rem" })
339
+ }
340
+ );
341
+ }
342
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid, { gap: 4, children: releases.map(({ id, name, actions, scheduledAt, status }) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.GridItem, { col: 3, s: 6, xs: 12, children: /* @__PURE__ */ jsxRuntime.jsx(LinkCard, { href: `content-releases/${id}`, isExternal: false, children: /* @__PURE__ */ jsxRuntime.jsxs(
343
+ designSystem.Flex,
344
+ {
345
+ direction: "column",
346
+ justifyContent: "space-between",
347
+ padding: 4,
348
+ hasRadius: true,
349
+ background: "neutral0",
350
+ shadow: "tableShadow",
351
+ height: "100%",
352
+ width: "100%",
353
+ alignItems: "start",
354
+ gap: 4,
355
+ children: [
356
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "start", gap: 1, children: [
357
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { as: "h3", variant: "delta", fontWeight: "bold", children: name }),
358
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: IsSchedulingEnabled ? scheduledAt ? /* @__PURE__ */ jsxRuntime.jsx(CapitalizeRelativeTime, { timestamp: new Date(scheduledAt) }) : formatMessage({
359
+ id: "content-releases.pages.Releases.not-scheduled",
360
+ defaultMessage: "Not scheduled"
361
+ }) : formatMessage(
362
+ {
363
+ id: "content-releases.page.Releases.release-item.entries",
364
+ defaultMessage: "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
365
+ },
366
+ { number: actions.meta.count }
367
+ ) })
368
+ ] }),
369
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { ...getBadgeProps(status), children: status })
370
+ ]
371
+ }
372
+ ) }) }, id)) });
373
+ };
374
+ const StyledAlert = styled__default.default(designSystem.Alert)`
375
+ button {
376
+ display: none;
377
+ }
378
+ p + div {
379
+ margin-left: auto;
380
+ }
381
+ `;
382
+ const INITIAL_FORM_VALUES = {
383
+ name: "",
384
+ date: null,
385
+ time: "",
386
+ // Remove future flag check after Scheduling Beta release and replace with true as creating new release should include scheduling by default
387
+ isScheduled: window.strapi.future.isEnabled("contentReleasesScheduling"),
388
+ scheduledAt: null,
389
+ timezone: null
390
+ };
391
+ const ReleasesPage = () => {
392
+ const tabRef = React__namespace.useRef(null);
393
+ const location = reactRouterDom.useLocation();
394
+ const [releaseModalShown, setReleaseModalShown] = React__namespace.useState(false);
395
+ const toggleNotification = helperPlugin.useNotification();
396
+ const { formatMessage } = reactIntl.useIntl();
397
+ const { push, replace } = reactRouterDom.useHistory();
398
+ const { formatAPIError } = helperPlugin.useAPIErrorHandler();
399
+ const [{ query }, setQuery] = helperPlugin.useQueryParams();
400
+ const response = index.useGetReleasesQuery(query);
401
+ const [createRelease, { isLoading: isSubmittingForm }] = index.useCreateReleaseMutation();
402
+ const { getFeature } = strapiAdmin.useLicenseLimits();
403
+ const { maximumReleases = 3 } = getFeature("cms-content-releases");
404
+ const { trackUsage } = helperPlugin.useTracking();
405
+ const { isLoading, isSuccess, isError } = response;
406
+ const activeTab = response?.currentData?.meta?.activeTab || "pending";
407
+ const activeTabIndex = ["pending", "done"].indexOf(activeTab);
408
+ React__namespace.useEffect(() => {
409
+ if (location?.state?.errors) {
410
+ toggleNotification({
411
+ type: "warning",
412
+ title: formatMessage({
413
+ id: "content-releases.pages.Releases.notification.error.title",
414
+ defaultMessage: "Your request could not be processed."
415
+ }),
416
+ message: formatMessage({
417
+ id: "content-releases.pages.Releases.notification.error.message",
418
+ defaultMessage: "Please try again or open another release."
419
+ })
420
+ });
421
+ replace({ state: null });
422
+ }
423
+ }, [formatMessage, location?.state?.errors, replace, toggleNotification]);
424
+ React__namespace.useEffect(() => {
425
+ if (tabRef.current) {
426
+ tabRef.current._handlers.setSelectedTabIndex(activeTabIndex);
427
+ }
428
+ }, [activeTabIndex]);
429
+ const toggleAddReleaseModal = () => {
430
+ setReleaseModalShown((prev) => !prev);
431
+ };
432
+ if (isLoading) {
433
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Main, { "aria-busy": isLoading, children: /* @__PURE__ */ jsxRuntime.jsx(helperPlugin.LoadingIndicatorPage, {}) });
434
+ }
435
+ const totalPendingReleases = isSuccess && response.currentData?.meta?.pendingReleasesCount || 0;
436
+ const hasReachedMaximumPendingReleases = totalPendingReleases >= maximumReleases;
437
+ const handleTabChange = (index2) => {
438
+ setQuery({
439
+ ...query,
440
+ page: 1,
441
+ pageSize: response?.currentData?.meta?.pagination?.pageSize || 16,
442
+ filters: {
443
+ releasedAt: {
444
+ $notNull: index2 === 0 ? false : true
445
+ }
446
+ }
447
+ });
448
+ };
449
+ const handleAddRelease = async ({ name, scheduledAt, timezone }) => {
450
+ const response2 = await createRelease({
451
+ name,
452
+ scheduledAt,
453
+ timezone
454
+ });
455
+ if ("data" in response2) {
456
+ toggleNotification({
457
+ type: "success",
458
+ message: formatMessage({
459
+ id: "content-releases.modal.release-created-notification-success",
460
+ defaultMessage: "Release created."
461
+ })
462
+ });
463
+ trackUsage("didCreateRelease");
464
+ push(`/plugins/content-releases/${response2.data.data.id}`);
465
+ } else if (index.isAxiosError(response2.error)) {
466
+ toggleNotification({
467
+ type: "warning",
468
+ message: formatAPIError(response2.error)
469
+ });
470
+ } else {
471
+ toggleNotification({
472
+ type: "warning",
473
+ message: formatMessage({ id: "notification.error", defaultMessage: "An error occurred" })
474
+ });
475
+ }
476
+ };
477
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Main, { "aria-busy": isLoading, children: [
478
+ /* @__PURE__ */ jsxRuntime.jsx(
479
+ designSystem.HeaderLayout,
480
+ {
481
+ title: formatMessage({
482
+ id: "content-releases.pages.Releases.title",
483
+ defaultMessage: "Releases"
484
+ }),
485
+ subtitle: formatMessage({
486
+ id: "content-releases.pages.Releases.header-subtitle",
487
+ defaultMessage: "Create and manage content updates"
488
+ }),
489
+ primaryAction: /* @__PURE__ */ jsxRuntime.jsx(helperPlugin.CheckPermissions, { permissions: index.PERMISSIONS.create, children: /* @__PURE__ */ jsxRuntime.jsx(
490
+ designSystem.Button,
491
+ {
492
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
493
+ onClick: toggleAddReleaseModal,
494
+ disabled: hasReachedMaximumPendingReleases,
495
+ children: formatMessage({
496
+ id: "content-releases.header.actions.add-release",
497
+ defaultMessage: "New release"
498
+ })
499
+ }
500
+ ) })
501
+ }
502
+ ),
503
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.ContentLayout, { children: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
504
+ hasReachedMaximumPendingReleases && /* @__PURE__ */ jsxRuntime.jsx(
505
+ StyledAlert,
506
+ {
507
+ marginBottom: 6,
508
+ action: /* @__PURE__ */ jsxRuntime.jsx(v2.Link, { href: "https://strapi.io/pricing-cloud", isExternal: true, children: formatMessage({
509
+ id: "content-releases.pages.Releases.max-limit-reached.action",
510
+ defaultMessage: "Explore plans"
511
+ }) }),
512
+ title: formatMessage(
513
+ {
514
+ id: "content-releases.pages.Releases.max-limit-reached.title",
515
+ defaultMessage: "You have reached the {number} pending {number, plural, one {release} other {releases}} limit."
516
+ },
517
+ { number: maximumReleases }
518
+ ),
519
+ onClose: () => {
520
+ },
521
+ closeLabel: "",
522
+ children: formatMessage({
523
+ id: "content-releases.pages.Releases.max-limit-reached.message",
524
+ defaultMessage: "Upgrade to manage an unlimited number of releases."
525
+ })
526
+ }
527
+ ),
528
+ /* @__PURE__ */ jsxRuntime.jsxs(
529
+ designSystem.TabGroup,
530
+ {
531
+ label: formatMessage({
532
+ id: "content-releases.pages.Releases.tab-group.label",
533
+ defaultMessage: "Releases list"
534
+ }),
535
+ variant: "simple",
536
+ initialSelectedTabIndex: activeTabIndex,
537
+ onTabChange: handleTabChange,
538
+ ref: tabRef,
539
+ children: [
540
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { paddingBottom: 8, children: [
541
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs, { children: [
542
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tab, { children: formatMessage(
543
+ {
544
+ id: "content-releases.pages.Releases.tab.pending",
545
+ defaultMessage: "Pending ({count})"
546
+ },
547
+ {
548
+ count: totalPendingReleases
549
+ }
550
+ ) }),
551
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tab, { children: formatMessage({
552
+ id: "content-releases.pages.Releases.tab.done",
553
+ defaultMessage: "Done"
554
+ }) })
555
+ ] }),
556
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, {})
557
+ ] }),
558
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.TabPanels, { children: [
559
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.TabPanel, { children: /* @__PURE__ */ jsxRuntime.jsx(
560
+ ReleasesGrid,
561
+ {
562
+ sectionTitle: "pending",
563
+ releases: response?.currentData?.data,
564
+ isError
565
+ }
566
+ ) }),
567
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.TabPanel, { children: /* @__PURE__ */ jsxRuntime.jsx(
568
+ ReleasesGrid,
569
+ {
570
+ sectionTitle: "done",
571
+ releases: response?.currentData?.data,
572
+ isError
573
+ }
574
+ ) })
575
+ ] })
576
+ ]
577
+ }
578
+ ),
579
+ response.currentData?.meta?.pagination?.total ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { paddingTop: 4, alignItems: "flex-end", justifyContent: "space-between", children: [
580
+ /* @__PURE__ */ jsxRuntime.jsx(
581
+ helperPlugin.PageSizeURLQuery,
582
+ {
583
+ options: ["8", "16", "32", "64"],
584
+ defaultValue: response?.currentData?.meta?.pagination?.pageSize.toString()
585
+ }
586
+ ),
587
+ /* @__PURE__ */ jsxRuntime.jsx(
588
+ helperPlugin.PaginationURLQuery,
589
+ {
590
+ pagination: {
591
+ pageCount: response?.currentData?.meta?.pagination?.pageCount || 0
592
+ }
593
+ }
594
+ )
595
+ ] }) : null
596
+ ] }) }),
597
+ releaseModalShown && /* @__PURE__ */ jsxRuntime.jsx(
598
+ ReleaseModal,
599
+ {
600
+ handleClose: toggleAddReleaseModal,
601
+ handleSubmit: handleAddRelease,
602
+ isLoading: isSubmittingForm,
603
+ initialValues: INITIAL_FORM_VALUES
604
+ }
605
+ )
606
+ ] });
607
+ };
111
608
  const ReleaseInfoWrapper = styled__default.default(designSystem.Flex)`
112
609
  align-self: stretch;
113
610
  border-bottom-right-radius: ${({ theme }) => theme.borderRadius};
114
611
  border-bottom-left-radius: ${({ theme }) => theme.borderRadius};
115
612
  border-top: 1px solid ${({ theme }) => theme.colors.neutral150};
116
613
  `;
117
- const StyledFlex = styled__default.default(designSystem.Flex)`
118
- align-self: stretch;
119
- cursor: ${({ disabled }) => disabled ? "not-allowed" : "pointer"};
120
-
614
+ const StyledMenuItem = styled__default.default(v2.Menu.Item)`
121
615
  svg path {
122
616
  fill: ${({ theme, disabled }) => disabled && theme.colors.neutral500};
123
617
  }
124
618
  span {
125
619
  color: ${({ theme, disabled }) => disabled && theme.colors.neutral500};
126
620
  }
621
+
622
+ &:hover {
623
+ background: ${({ theme, variant = "neutral" }) => theme.colors[`${variant}100`]};
624
+ }
127
625
  `;
128
626
  const PencilIcon = styled__default.default(icons.Pencil)`
129
- width: ${({ theme }) => theme.spaces[4]};
130
- height: ${({ theme }) => theme.spaces[4]};
627
+ width: ${({ theme }) => theme.spaces[3]};
628
+ height: ${({ theme }) => theme.spaces[3]};
131
629
  path {
132
630
  fill: ${({ theme }) => theme.colors.neutral600};
133
631
  }
134
632
  `;
135
633
  const TrashIcon = styled__default.default(icons.Trash)`
136
- width: ${({ theme }) => theme.spaces[4]};
137
- height: ${({ theme }) => theme.spaces[4]};
634
+ width: ${({ theme }) => theme.spaces[3]};
635
+ height: ${({ theme }) => theme.spaces[3]};
138
636
  path {
139
637
  fill: ${({ theme }) => theme.colors.danger600};
140
638
  }
@@ -142,24 +640,6 @@ const TrashIcon = styled__default.default(icons.Trash)`
142
640
  const TypographyMaxWidth = styled__default.default(designSystem.Typography)`
143
641
  max-width: 300px;
144
642
  `;
145
- const PopoverButton = ({ onClick, disabled, children }) => {
146
- return /* @__PURE__ */ jsxRuntime.jsx(
147
- StyledFlex,
148
- {
149
- paddingTop: 2,
150
- paddingBottom: 2,
151
- paddingLeft: 4,
152
- paddingRight: 4,
153
- alignItems: "center",
154
- gap: 2,
155
- as: "button",
156
- hasRadius: true,
157
- onClick,
158
- disabled,
159
- children
160
- }
161
- );
162
- };
163
643
  const EntryValidationText = ({ action, schema, components, entry }) => {
164
644
  const { formatMessage } = reactIntl.useIntl();
165
645
  const { validate } = strapiAdmin.unstable_useDocument();
@@ -208,10 +688,8 @@ const ReleaseDetailsLayout = ({
208
688
  toggleWarningSubmit,
209
689
  children
210
690
  }) => {
211
- const { formatMessage } = reactIntl.useIntl();
691
+ const { formatMessage, formatDate, formatTime } = reactIntl.useIntl();
212
692
  const { releaseId } = reactRouterDom.useParams();
213
- const [isPopoverVisible, setIsPopoverVisible] = React__namespace.useState(false);
214
- const moreButtonRef = React__namespace.useRef(null);
215
693
  const {
216
694
  data,
217
695
  isLoading: isLoadingDetails,
@@ -227,13 +705,6 @@ const ReleaseDetailsLayout = ({
227
705
  const dispatch = index.useTypedDispatch();
228
706
  const { trackUsage } = helperPlugin.useTracking();
229
707
  const release = data?.data;
230
- const handleTogglePopover = () => {
231
- setIsPopoverVisible((prev) => !prev);
232
- };
233
- const openReleaseModal = () => {
234
- toggleEditReleaseModal();
235
- handleTogglePopover();
236
- };
237
708
  const handlePublishRelease = async () => {
238
709
  const response = await publishRelease({ id: releaseId });
239
710
  if ("data" in response) {
@@ -262,13 +733,21 @@ const ReleaseDetailsLayout = ({
262
733
  });
263
734
  }
264
735
  };
265
- const openWarningConfirmDialog = () => {
266
- toggleWarningSubmit();
267
- handleTogglePopover();
268
- };
269
736
  const handleRefresh = () => {
270
737
  dispatch(index.releaseApi.util.invalidateTags([{ type: "ReleaseAction", id: "LIST" }]));
271
738
  };
739
+ const getCreatedByUser = () => {
740
+ if (!release?.createdBy) {
741
+ return null;
742
+ }
743
+ if (release.createdBy.username) {
744
+ return release.createdBy.username;
745
+ }
746
+ if (release.createdBy.firstname) {
747
+ return `${release.createdBy.firstname} ${release.createdBy.lastname || ""}`.trim();
748
+ }
749
+ return release.createdBy.email;
750
+ };
272
751
  if (isLoadingDetails) {
273
752
  return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Main, { "aria-busy": isLoadingDetails, children: /* @__PURE__ */ jsxRuntime.jsx(helperPlugin.LoadingIndicatorPage, {}) });
274
753
  }
@@ -290,90 +769,128 @@ const ReleaseDetailsLayout = ({
290
769
  );
291
770
  }
292
771
  const totalEntries = release.actions.meta.count || 0;
293
- const createdBy = release.createdBy.lastname ? `${release.createdBy.firstname} ${release.createdBy.lastname}` : `${release.createdBy.firstname}`;
772
+ const hasCreatedByUser = Boolean(getCreatedByUser());
773
+ const IsSchedulingEnabled = window.strapi.future.isEnabled("contentReleasesScheduling");
774
+ const isScheduled = release.scheduledAt && release.timezone;
775
+ const numberOfEntriesText = formatMessage(
776
+ {
777
+ id: "content-releases.pages.Details.header-subtitle",
778
+ defaultMessage: "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
779
+ },
780
+ { number: totalEntries }
781
+ );
782
+ const scheduledText = isScheduled ? formatMessage(
783
+ {
784
+ id: "content-releases.pages.ReleaseDetails.header-subtitle.scheduled",
785
+ defaultMessage: "Scheduled for {date} at {time} ({offset})"
786
+ },
787
+ {
788
+ date: formatDate(new Date(release.scheduledAt), {
789
+ weekday: "long",
790
+ day: "numeric",
791
+ month: "long",
792
+ year: "numeric",
793
+ timeZone: release.timezone
794
+ }),
795
+ time: formatTime(new Date(release.scheduledAt), {
796
+ timeZone: release.timezone,
797
+ hourCycle: "h23"
798
+ }),
799
+ offset: index.getTimezoneOffset(release.timezone, new Date(release.scheduledAt))
800
+ }
801
+ ) : "";
294
802
  return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Main, { "aria-busy": isLoadingDetails, children: [
295
803
  /* @__PURE__ */ jsxRuntime.jsx(
296
804
  designSystem.HeaderLayout,
297
805
  {
298
806
  title: release.name,
299
- subtitle: formatMessage(
300
- {
301
- id: "content-releases.pages.Details.header-subtitle",
302
- defaultMessage: "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
303
- },
304
- { number: totalEntries }
305
- ),
807
+ subtitle: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, lineHeight: 6, children: [
808
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", variant: "epsilon", children: numberOfEntriesText + (IsSchedulingEnabled && isScheduled ? ` - ${scheduledText}` : "") }),
809
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { ...getBadgeProps(release.status), children: release.status })
810
+ ] }),
306
811
  navigationAction: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Link, { startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowLeft, {}), to: "/plugins/content-releases", children: formatMessage({
307
812
  id: "global.back",
308
813
  defaultMessage: "Back"
309
814
  }) }),
310
815
  primaryAction: !release.releasedAt && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
311
- /* @__PURE__ */ jsxRuntime.jsx(
312
- designSystem.IconButton,
313
- {
314
- label: formatMessage({
315
- id: "content-releases.header.actions.open-release-actions",
316
- defaultMessage: "Release actions"
317
- }),
318
- ref: moreButtonRef,
319
- onClick: handleTogglePopover,
320
- children: /* @__PURE__ */ jsxRuntime.jsx(icons.More, {})
321
- }
322
- ),
323
- isPopoverVisible && /* @__PURE__ */ jsxRuntime.jsxs(
324
- designSystem.Popover,
325
- {
326
- source: moreButtonRef,
327
- placement: "bottom-end",
328
- onDismiss: handleTogglePopover,
329
- spacing: 4,
330
- minWidth: "242px",
331
- children: [
332
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", justifyContent: "center", direction: "column", padding: 1, children: [
333
- /* @__PURE__ */ jsxRuntime.jsxs(PopoverButton, { disabled: !canUpdate, onClick: openReleaseModal, children: [
334
- /* @__PURE__ */ jsxRuntime.jsx(PencilIcon, {}),
335
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { ellipsis: true, children: formatMessage({
336
- id: "content-releases.header.actions.edit",
337
- defaultMessage: "Edit"
338
- }) })
339
- ] }),
340
- /* @__PURE__ */ jsxRuntime.jsxs(PopoverButton, { disabled: !canDelete, onClick: openWarningConfirmDialog, children: [
341
- /* @__PURE__ */ jsxRuntime.jsx(TrashIcon, {}),
342
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { ellipsis: true, textColor: "danger600", children: formatMessage({
343
- id: "content-releases.header.actions.delete",
344
- defaultMessage: "Delete"
345
- }) })
346
- ] })
347
- ] }),
348
- /* @__PURE__ */ jsxRuntime.jsxs(
349
- ReleaseInfoWrapper,
350
- {
351
- direction: "column",
352
- justifyContent: "center",
353
- alignItems: "flex-start",
354
- gap: 1,
355
- padding: 5,
356
- children: [
357
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", children: formatMessage({
358
- id: "content-releases.header.actions.created",
359
- defaultMessage: "Created"
360
- }) }),
361
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", color: "neutral300", children: [
362
- /* @__PURE__ */ jsxRuntime.jsx(helperPlugin.RelativeTime, { timestamp: new Date(release.createdAt) }),
363
- formatMessage(
364
- {
365
- id: "content-releases.header.actions.created.description",
366
- defaultMessage: " by {createdBy}"
367
- },
368
- { createdBy }
369
- )
370
- ] })
371
- ]
372
- }
373
- )
374
- ]
375
- }
376
- ),
816
+ /* @__PURE__ */ jsxRuntime.jsxs(v2.Menu.Root, { children: [
817
+ /* @__PURE__ */ jsxRuntime.jsx(
818
+ v2.Menu.Trigger,
819
+ {
820
+ as: designSystem.IconButton,
821
+ paddingLeft: 2,
822
+ paddingRight: 2,
823
+ "aria-label": formatMessage({
824
+ id: "content-releases.header.actions.open-release-actions",
825
+ defaultMessage: "Release edit and delete menu"
826
+ }),
827
+ icon: /* @__PURE__ */ jsxRuntime.jsx(icons.More, {}),
828
+ variant: "tertiary"
829
+ }
830
+ ),
831
+ /* @__PURE__ */ jsxRuntime.jsxs(v2.Menu.Content, { top: 1, popoverPlacement: "bottom-end", children: [
832
+ /* @__PURE__ */ jsxRuntime.jsxs(
833
+ designSystem.Flex,
834
+ {
835
+ alignItems: "center",
836
+ justifyContent: "center",
837
+ direction: "column",
838
+ padding: 1,
839
+ width: "100%",
840
+ children: [
841
+ /* @__PURE__ */ jsxRuntime.jsx(StyledMenuItem, { disabled: !canUpdate, onSelect: toggleEditReleaseModal, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, hasRadius: true, width: "100%", children: [
842
+ /* @__PURE__ */ jsxRuntime.jsx(PencilIcon, {}),
843
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { ellipsis: true, children: formatMessage({
844
+ id: "content-releases.header.actions.edit",
845
+ defaultMessage: "Edit"
846
+ }) })
847
+ ] }) }),
848
+ /* @__PURE__ */ jsxRuntime.jsx(
849
+ StyledMenuItem,
850
+ {
851
+ disabled: !canDelete,
852
+ onSelect: toggleWarningSubmit,
853
+ variant: "danger",
854
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, hasRadius: true, width: "100%", children: [
855
+ /* @__PURE__ */ jsxRuntime.jsx(TrashIcon, {}),
856
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { ellipsis: true, textColor: "danger600", children: formatMessage({
857
+ id: "content-releases.header.actions.delete",
858
+ defaultMessage: "Delete"
859
+ }) })
860
+ ] })
861
+ }
862
+ )
863
+ ]
864
+ }
865
+ ),
866
+ /* @__PURE__ */ jsxRuntime.jsxs(
867
+ ReleaseInfoWrapper,
868
+ {
869
+ direction: "column",
870
+ justifyContent: "center",
871
+ alignItems: "flex-start",
872
+ gap: 1,
873
+ padding: 5,
874
+ children: [
875
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", children: formatMessage({
876
+ id: "content-releases.header.actions.created",
877
+ defaultMessage: "Created"
878
+ }) }),
879
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", color: "neutral300", children: [
880
+ /* @__PURE__ */ jsxRuntime.jsx(helperPlugin.RelativeTime, { timestamp: new Date(release.createdAt) }),
881
+ formatMessage(
882
+ {
883
+ id: "content-releases.header.actions.created.description",
884
+ defaultMessage: "{hasCreatedByUser, select, true { by {createdBy}} other { by deleted user}}"
885
+ },
886
+ { createdBy: getCreatedByUser(), hasCreatedByUser }
887
+ )
888
+ ] })
889
+ ]
890
+ }
891
+ )
892
+ ] })
893
+ ] }),
377
894
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { size: "S", variant: "tertiary", onClick: handleRefresh, children: formatMessage({
378
895
  id: "content-releases.header.actions.refresh",
379
896
  defaultMessage: "Refresh"
@@ -429,6 +946,9 @@ const ReleaseDetailsBody = () => {
429
946
  isError: isReleaseError,
430
947
  error: releaseError
431
948
  } = index.useGetReleaseQuery({ id: releaseId });
949
+ const {
950
+ allowedActions: { canUpdate }
951
+ } = helperPlugin.useRBAC(index.PERMISSIONS);
432
952
  const release = releaseData?.data;
433
953
  const selectedGroupBy = query?.groupBy || "contentType";
434
954
  const {
@@ -535,7 +1055,7 @@ const ReleaseDetailsBody = () => {
535
1055
  designSystem.SingleSelect,
536
1056
  {
537
1057
  "aria-label": formatMessage({
538
- id: "content-releases.pages.ReleaseDetails.groupBy.label",
1058
+ id: "content-releases.pages.ReleaseDetails.groupBy.aria-label",
539
1059
  defaultMessage: "Group by"
540
1060
  }),
541
1061
  customizeContent: (value) => formatMessage(
@@ -553,7 +1073,7 @@ const ReleaseDetailsBody = () => {
553
1073
  }
554
1074
  ) }),
555
1075
  Object.keys(releaseActions).map((key) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, direction: "column", alignItems: "stretch", children: [
556
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { children: key }) }),
1076
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { role: "separator", "aria-label": key, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { children: key }) }),
557
1077
  /* @__PURE__ */ jsxRuntime.jsx(
558
1078
  helperPlugin.Table.Root,
559
1079
  {
@@ -642,7 +1162,8 @@ const ReleaseDetailsBody = () => {
642
1162
  {
643
1163
  selected: type,
644
1164
  handleChange: (e) => handleChangeType(e, id, [key, actionIndex]),
645
- name: `release-action-${id}-type`
1165
+ name: `release-action-${id}-type`,
1166
+ disabled: !canUpdate
646
1167
  }
647
1168
  ) }),
648
1169
  !release.releasedAt && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -721,11 +1242,18 @@ const ReleaseDetailsPage = () => {
721
1242
  }
722
1243
  );
723
1244
  }
724
- const title = isSuccessDetails && data?.data?.name || "";
1245
+ const releaseData = isSuccessDetails && data?.data || null;
1246
+ const title = releaseData?.name || "";
1247
+ const timezone = releaseData?.timezone ?? null;
1248
+ const scheduledAt = releaseData?.scheduledAt && timezone ? dateFnsTz.utcToZonedTime(releaseData.scheduledAt, timezone) : null;
1249
+ const date = scheduledAt ? new Date(format__default.default(scheduledAt, "yyyy-MM-dd")) : null;
1250
+ const time = scheduledAt ? format__default.default(scheduledAt, "HH:mm") : "";
725
1251
  const handleEditRelease = async (values) => {
726
1252
  const response = await updateRelease({
727
1253
  id: releaseId,
728
- name: values.name
1254
+ name: values.name,
1255
+ scheduledAt: values.scheduledAt,
1256
+ timezone: values.timezone
729
1257
  });
730
1258
  if ("data" in response) {
731
1259
  toggleNotification({
@@ -779,7 +1307,14 @@ const ReleaseDetailsPage = () => {
779
1307
  handleClose: toggleEditReleaseModal,
780
1308
  handleSubmit: handleEditRelease,
781
1309
  isLoading: isLoadingDetails || isSubmittingForm,
782
- initialValues: { name: title || "" }
1310
+ initialValues: {
1311
+ name: title || "",
1312
+ scheduledAt,
1313
+ date,
1314
+ time,
1315
+ isScheduled: Boolean(scheduledAt),
1316
+ timezone
1317
+ }
783
1318
  }
784
1319
  ),
785
1320
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -799,281 +1334,6 @@ const ReleaseDetailsPage = () => {
799
1334
  }
800
1335
  );
801
1336
  };
802
- const LinkCard = styled__default.default(v2.Link)`
803
- display: block;
804
- `;
805
- const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }) => {
806
- const { formatMessage } = reactIntl.useIntl();
807
- if (isError) {
808
- return /* @__PURE__ */ jsxRuntime.jsx(helperPlugin.AnErrorOccurred, {});
809
- }
810
- if (releases?.length === 0) {
811
- return /* @__PURE__ */ jsxRuntime.jsx(
812
- designSystem.EmptyStateLayout,
813
- {
814
- content: formatMessage(
815
- {
816
- id: "content-releases.page.Releases.tab.emptyEntries",
817
- defaultMessage: "No releases"
818
- },
819
- {
820
- target: sectionTitle
821
- }
822
- ),
823
- icon: /* @__PURE__ */ jsxRuntime.jsx(icons.EmptyDocuments, { width: "10rem" })
824
- }
825
- );
826
- }
827
- return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid, { gap: 4, children: releases.map(({ id, name, actions }) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.GridItem, { col: 3, s: 6, xs: 12, children: /* @__PURE__ */ jsxRuntime.jsx(LinkCard, { href: `content-releases/${id}`, isExternal: false, children: /* @__PURE__ */ jsxRuntime.jsxs(
828
- designSystem.Flex,
829
- {
830
- direction: "column",
831
- justifyContent: "space-between",
832
- padding: 4,
833
- hasRadius: true,
834
- background: "neutral0",
835
- shadow: "tableShadow",
836
- height: "100%",
837
- width: "100%",
838
- alignItems: "start",
839
- gap: 2,
840
- children: [
841
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { as: "h3", variant: "delta", fontWeight: "bold", children: name }),
842
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", children: formatMessage(
843
- {
844
- id: "content-releases.page.Releases.release-item.entries",
845
- defaultMessage: "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
846
- },
847
- { number: actions.meta.count }
848
- ) })
849
- ]
850
- }
851
- ) }) }, id)) });
852
- };
853
- const StyledAlert = styled__default.default(designSystem.Alert)`
854
- button {
855
- display: none;
856
- }
857
- p + div {
858
- margin-left: auto;
859
- }
860
- `;
861
- const INITIAL_FORM_VALUES = {
862
- name: ""
863
- };
864
- const ReleasesPage = () => {
865
- const tabRef = React__namespace.useRef(null);
866
- const location = reactRouterDom.useLocation();
867
- const [releaseModalShown, setReleaseModalShown] = React__namespace.useState(false);
868
- const toggleNotification = helperPlugin.useNotification();
869
- const { formatMessage } = reactIntl.useIntl();
870
- const { push, replace } = reactRouterDom.useHistory();
871
- const { formatAPIError } = helperPlugin.useAPIErrorHandler();
872
- const [{ query }, setQuery] = helperPlugin.useQueryParams();
873
- const response = index.useGetReleasesQuery(query);
874
- const [createRelease, { isLoading: isSubmittingForm }] = index.useCreateReleaseMutation();
875
- const { getFeature } = strapiAdmin.useLicenseLimits();
876
- const { maximumReleases = 3 } = getFeature("cms-content-releases");
877
- const { trackUsage } = helperPlugin.useTracking();
878
- const { isLoading, isSuccess, isError } = response;
879
- const activeTab = response?.currentData?.meta?.activeTab || "pending";
880
- const activeTabIndex = ["pending", "done"].indexOf(activeTab);
881
- React__namespace.useEffect(() => {
882
- if (location?.state?.errors) {
883
- toggleNotification({
884
- type: "warning",
885
- title: formatMessage({
886
- id: "content-releases.pages.Releases.notification.error.title",
887
- defaultMessage: "Your request could not be processed."
888
- }),
889
- message: formatMessage({
890
- id: "content-releases.pages.Releases.notification.error.message",
891
- defaultMessage: "Please try again or open another release."
892
- })
893
- });
894
- replace({ state: null });
895
- }
896
- }, [formatMessage, location?.state?.errors, replace, toggleNotification]);
897
- React__namespace.useEffect(() => {
898
- if (tabRef.current) {
899
- tabRef.current._handlers.setSelectedTabIndex(activeTabIndex);
900
- }
901
- }, [activeTabIndex]);
902
- const toggleAddReleaseModal = () => {
903
- setReleaseModalShown((prev) => !prev);
904
- };
905
- if (isLoading) {
906
- return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Main, { "aria-busy": isLoading, children: /* @__PURE__ */ jsxRuntime.jsx(helperPlugin.LoadingIndicatorPage, {}) });
907
- }
908
- const totalReleases = isSuccess && response.currentData?.meta?.pagination?.total || 0;
909
- const hasReachedMaximumPendingReleases = totalReleases >= maximumReleases;
910
- const handleTabChange = (index2) => {
911
- setQuery({
912
- ...query,
913
- page: 1,
914
- pageSize: response?.currentData?.meta?.pagination?.pageSize || 16,
915
- filters: {
916
- releasedAt: {
917
- $notNull: index2 === 0 ? false : true
918
- }
919
- }
920
- });
921
- };
922
- const handleAddRelease = async (values) => {
923
- const response2 = await createRelease({
924
- name: values.name
925
- });
926
- if ("data" in response2) {
927
- toggleNotification({
928
- type: "success",
929
- message: formatMessage({
930
- id: "content-releases.modal.release-created-notification-success",
931
- defaultMessage: "Release created."
932
- })
933
- });
934
- trackUsage("didCreateRelease");
935
- push(`/plugins/content-releases/${response2.data.data.id}`);
936
- } else if (index.isAxiosError(response2.error)) {
937
- toggleNotification({
938
- type: "warning",
939
- message: formatAPIError(response2.error)
940
- });
941
- } else {
942
- toggleNotification({
943
- type: "warning",
944
- message: formatMessage({ id: "notification.error", defaultMessage: "An error occurred" })
945
- });
946
- }
947
- };
948
- return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Main, { "aria-busy": isLoading, children: [
949
- /* @__PURE__ */ jsxRuntime.jsx(
950
- designSystem.HeaderLayout,
951
- {
952
- title: formatMessage({
953
- id: "content-releases.pages.Releases.title",
954
- defaultMessage: "Releases"
955
- }),
956
- subtitle: formatMessage(
957
- {
958
- id: "content-releases.pages.Releases.header-subtitle",
959
- defaultMessage: "{number, plural, =0 {No releases} one {# release} other {# releases}}"
960
- },
961
- { number: totalReleases }
962
- ),
963
- primaryAction: /* @__PURE__ */ jsxRuntime.jsx(helperPlugin.CheckPermissions, { permissions: index.PERMISSIONS.create, children: /* @__PURE__ */ jsxRuntime.jsx(
964
- designSystem.Button,
965
- {
966
- startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
967
- onClick: toggleAddReleaseModal,
968
- disabled: hasReachedMaximumPendingReleases,
969
- children: formatMessage({
970
- id: "content-releases.header.actions.add-release",
971
- defaultMessage: "New release"
972
- })
973
- }
974
- ) })
975
- }
976
- ),
977
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.ContentLayout, { children: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
978
- activeTab === "pending" && hasReachedMaximumPendingReleases && /* @__PURE__ */ jsxRuntime.jsx(
979
- StyledAlert,
980
- {
981
- marginBottom: 6,
982
- action: /* @__PURE__ */ jsxRuntime.jsx(v2.Link, { href: "https://strapi.io/pricing-cloud", isExternal: true, children: formatMessage({
983
- id: "content-releases.pages.Releases.max-limit-reached.action",
984
- defaultMessage: "Explore plans"
985
- }) }),
986
- title: formatMessage(
987
- {
988
- id: "content-releases.pages.Releases.max-limit-reached.title",
989
- defaultMessage: "You have reached the {number} pending {number, plural, one {release} other {releases}} limit."
990
- },
991
- { number: maximumReleases }
992
- ),
993
- onClose: () => {
994
- },
995
- closeLabel: "",
996
- children: formatMessage({
997
- id: "content-releases.pages.Releases.max-limit-reached.message",
998
- defaultMessage: "Upgrade to manage an unlimited number of releases."
999
- })
1000
- }
1001
- ),
1002
- /* @__PURE__ */ jsxRuntime.jsxs(
1003
- designSystem.TabGroup,
1004
- {
1005
- label: formatMessage({
1006
- id: "content-releases.pages.Releases.tab-group.label",
1007
- defaultMessage: "Releases list"
1008
- }),
1009
- variant: "simple",
1010
- initialSelectedTabIndex: activeTabIndex,
1011
- onTabChange: handleTabChange,
1012
- ref: tabRef,
1013
- children: [
1014
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { paddingBottom: 8, children: [
1015
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs, { children: [
1016
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tab, { children: formatMessage({
1017
- id: "content-releases.pages.Releases.tab.pending",
1018
- defaultMessage: "Pending"
1019
- }) }),
1020
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tab, { children: formatMessage({
1021
- id: "content-releases.pages.Releases.tab.done",
1022
- defaultMessage: "Done"
1023
- }) })
1024
- ] }),
1025
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, {})
1026
- ] }),
1027
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.TabPanels, { children: [
1028
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.TabPanel, { children: /* @__PURE__ */ jsxRuntime.jsx(
1029
- ReleasesGrid,
1030
- {
1031
- sectionTitle: "pending",
1032
- releases: response?.currentData?.data,
1033
- isError
1034
- }
1035
- ) }),
1036
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.TabPanel, { children: /* @__PURE__ */ jsxRuntime.jsx(
1037
- ReleasesGrid,
1038
- {
1039
- sectionTitle: "done",
1040
- releases: response?.currentData?.data,
1041
- isError
1042
- }
1043
- ) })
1044
- ] })
1045
- ]
1046
- }
1047
- ),
1048
- totalReleases > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { paddingTop: 4, alignItems: "flex-end", justifyContent: "space-between", children: [
1049
- /* @__PURE__ */ jsxRuntime.jsx(
1050
- helperPlugin.PageSizeURLQuery,
1051
- {
1052
- options: ["8", "16", "32", "64"],
1053
- defaultValue: response?.currentData?.meta?.pagination?.pageSize.toString()
1054
- }
1055
- ),
1056
- /* @__PURE__ */ jsxRuntime.jsx(
1057
- helperPlugin.PaginationURLQuery,
1058
- {
1059
- pagination: {
1060
- pageCount: response?.currentData?.meta?.pagination?.pageCount || 0
1061
- }
1062
- }
1063
- )
1064
- ] })
1065
- ] }) }),
1066
- releaseModalShown && /* @__PURE__ */ jsxRuntime.jsx(
1067
- ReleaseModal,
1068
- {
1069
- handleClose: toggleAddReleaseModal,
1070
- handleSubmit: handleAddRelease,
1071
- isLoading: isSubmittingForm,
1072
- initialValues: INITIAL_FORM_VALUES
1073
- }
1074
- )
1075
- ] });
1076
- };
1077
1337
  const App = () => {
1078
1338
  return /* @__PURE__ */ jsxRuntime.jsx(helperPlugin.CheckPagePermissions, { permissions: index.PERMISSIONS.main, children: /* @__PURE__ */ jsxRuntime.jsxs(reactRouterDom.Switch, { children: [
1079
1339
  /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Route, { exact: true, path: `/plugins/${index.pluginId}`, component: ReleasesPage }),
@@ -1081,4 +1341,4 @@ const App = () => {
1081
1341
  ] }) });
1082
1342
  };
1083
1343
  exports.App = App;
1084
- //# sourceMappingURL=App-5PsAyVt2.js.map
1344
+ //# sourceMappingURL=App-P1kyM3gT.js.map