@strapi/content-releases 0.0.0-experimental.cae3a5a17d131a6f59673b62d01cfac869ea9cc2 → 0.0.0-experimental.d4cb32ce579e12a4436d68036f2327132fba1309

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-iqqoPnBO.js → App-OK4Xac-O.js} +404 -164
  2. package/dist/_chunks/App-OK4Xac-O.js.map +1 -0
  3. package/dist/_chunks/{App-_Jj3tWts.mjs → App-xAkiD42p.mjs} +407 -168
  4. package/dist/_chunks/App-xAkiD42p.mjs.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-2DuPv5k0.js → en-r0otWaln.js} +15 -4
  10. package/dist/_chunks/en-r0otWaln.js.map +1 -0
  11. package/dist/_chunks/{en-SOqjCdyh.mjs → en-veqvqeEr.mjs} +15 -4
  12. package/dist/_chunks/en-veqvqeEr.mjs.map +1 -0
  13. package/dist/_chunks/{index-_lT-gI3M.js → index-JvA2_26n.js} +113 -26
  14. package/dist/_chunks/index-JvA2_26n.js.map +1 -0
  15. package/dist/_chunks/{index-bsuc8ZwZ.mjs → index-exoiSU3V.mjs} +118 -31
  16. package/dist/_chunks/index-exoiSU3V.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 +597 -382
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +597 -382
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +12 -9
  24. package/dist/_chunks/App-_Jj3tWts.mjs.map +0 -1
  25. package/dist/_chunks/App-iqqoPnBO.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-_lT-gI3M.js.map +0 -1
  29. package/dist/_chunks/index-bsuc8ZwZ.mjs.map +0 -1
@@ -1,22 +1,42 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
- import { useNotification, useAPIErrorHandler, LoadingIndicatorPage, ConfirmDialog, useRBAC, RelativeTime, CheckPermissions, useQueryParams, AnErrorOccurred, NoContent, Table, PageSizeURLQuery, PaginationURLQuery, CheckPagePermissions } from "@strapi/helper-plugin";
2
+ import { useNotification, useAPIErrorHandler, LoadingIndicatorPage, ConfirmDialog, useRBAC, useTracking, RelativeTime, CheckPermissions, useQueryParams, AnErrorOccurred, NoContent, Table, PageSizeURLQuery, PaginationURLQuery, CheckPagePermissions } from "@strapi/helper-plugin";
3
3
  import { useLocation, useParams, useHistory, Redirect, Link as Link$1, Switch, Route } from "react-router-dom";
4
- import { p as pluginId, u as useGetReleaseQuery, a as useUpdateReleaseMutation, b as useDeleteReleaseMutation, c as usePublishReleaseMutation, P as PERMISSIONS, d as useTypedDispatch, e as useGetReleaseActionsQuery, f as useUpdateReleaseActionMutation, R as ReleaseActionOptions, g as ReleaseActionMenu, i as isAxiosError, r as releaseApi, h as useGetReleasesQuery, j as useCreateReleaseMutation } from "./index-bsuc8ZwZ.mjs";
4
+ import { g as getTimezoneOffset, p as pluginId, u as useGetReleaseQuery, a as useUpdateReleaseMutation, b as useDeleteReleaseMutation, c as usePublishReleaseMutation, P as PERMISSIONS, d as useTypedDispatch, e as useGetReleaseActionsQuery, f as useUpdateReleaseActionMutation, R as ReleaseActionOptions, h as ReleaseActionMenu, i as isAxiosError, r as releaseApi, j as useGetReleasesQuery, k as useCreateReleaseMutation } from "./index-exoiSU3V.mjs";
5
5
  import * as React from "react";
6
6
  import { unstable_useDocument, useLicenseLimits } from "@strapi/admin/strapi-admin";
7
- import { ModalLayout, ModalHeader, Typography, ModalBody, TextInput, ModalFooter, Button, Flex, ContentLayout, Main, HeaderLayout, Link, IconButton, Popover, SingleSelect, SingleSelectOption, Badge, Tr, Td, Icon, Tooltip, Alert, TabGroup, Box, Tabs, Tab, Divider, TabPanels, TabPanel, EmptyStateLayout, Grid, GridItem } from "@strapi/design-system";
8
- import { LinkButton, Link as Link$2 } from "@strapi/design-system/v2";
7
+ import { ModalLayout, ModalHeader, Typography, ModalBody, Flex, TextInput, Box, Checkbox, DatePicker, TimePicker, ModalFooter, Button, Combobox, ComboboxOption, ContentLayout, Main, HeaderLayout, Link, IconButton, SingleSelect, SingleSelectOption, Badge, Tr, Td, Icon, Tooltip, Alert, TabGroup, Tabs, Tab, Divider, TabPanels, TabPanel, EmptyStateLayout, Grid, GridItem } from "@strapi/design-system";
8
+ import { Menu, LinkButton, Link as Link$2 } from "@strapi/design-system/v2";
9
9
  import { Pencil, Trash, ArrowLeft, More, CrossCircle, CheckCircle, Plus, EmptyDocuments } from "@strapi/icons";
10
+ import format from "date-fns/format";
11
+ import { zonedTimeToUtc, utcToZonedTime } from "date-fns-tz";
10
12
  import { useIntl } from "react-intl";
11
13
  import styled from "styled-components";
12
- import { Formik, Form } from "formik";
14
+ import { formatISO, parse } from "date-fns";
15
+ import { Formik, Form, useFormikContext } from "formik";
13
16
  import * as yup from "yup";
14
17
  import "@reduxjs/toolkit/query";
15
18
  import "axios";
16
19
  import "@reduxjs/toolkit/query/react";
17
20
  import "react-redux";
18
21
  const RELEASE_SCHEMA = yup.object().shape({
19
- name: yup.string().trim().required()
22
+ name: yup.string().trim().required(),
23
+ scheduledAt: yup.string().nullable(),
24
+ isScheduled: yup.boolean().optional(),
25
+ time: yup.string().when("isScheduled", {
26
+ is: true,
27
+ then: yup.string().trim().required(),
28
+ otherwise: yup.string().nullable()
29
+ }),
30
+ timezone: yup.string().when("isScheduled", {
31
+ is: true,
32
+ then: yup.string().required().nullable(),
33
+ otherwise: yup.string().nullable()
34
+ }),
35
+ date: yup.string().when("isScheduled", {
36
+ is: true,
37
+ then: yup.string().required().nullable(),
38
+ otherwise: yup.string().nullable()
39
+ })
20
40
  }).required().noUnknown();
21
41
  const ReleaseModal = ({
22
42
  handleClose,
@@ -27,6 +47,24 @@ const ReleaseModal = ({
27
47
  const { formatMessage } = useIntl();
28
48
  const { pathname } = useLocation();
29
49
  const isCreatingRelease = pathname === `/plugins/${pluginId}`;
50
+ const IsSchedulingEnabled = window.strapi.future.isEnabled("contentReleasesScheduling");
51
+ const { timezoneList, systemTimezone = { value: "UTC+00:00-Africa/Abidjan " } } = getTimezones(
52
+ initialValues.scheduledAt ? new Date(initialValues.scheduledAt) : /* @__PURE__ */ new Date()
53
+ );
54
+ const getScheduledTimestamp = (values) => {
55
+ const { date, time, timezone } = values;
56
+ if (!date || !time || !timezone)
57
+ return null;
58
+ const formattedDate = parse(time, "HH:mm", new Date(date));
59
+ const timezoneWithoutOffset = timezone.split("_")[1];
60
+ return zonedTimeToUtc(formattedDate, timezoneWithoutOffset);
61
+ };
62
+ const getTimezoneWithOffset = () => {
63
+ const currentTimezone = timezoneList.find(
64
+ (timezone) => timezone.value.split("_")[1] === initialValues.timezone
65
+ );
66
+ return currentTimezone?.value || systemTimezone.value;
67
+ };
30
68
  return /* @__PURE__ */ jsxs(ModalLayout, { onClose: handleClose, labelledBy: "title", children: [
31
69
  /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(Typography, { id: "title", fontWeight: "bold", textColor: "neutral800", children: formatMessage(
32
70
  {
@@ -38,45 +76,134 @@ const ReleaseModal = ({
38
76
  /* @__PURE__ */ jsx(
39
77
  Formik,
40
78
  {
41
- validateOnChange: false,
42
- onSubmit: handleSubmit,
43
- initialValues,
79
+ onSubmit: (values) => {
80
+ handleSubmit({
81
+ ...values,
82
+ timezone: values.timezone ? values.timezone.split("_")[1] : null,
83
+ scheduledAt: values.isScheduled ? getScheduledTimestamp(values) : null
84
+ });
85
+ },
86
+ initialValues: {
87
+ ...initialValues,
88
+ timezone: initialValues.timezone ? getTimezoneWithOffset() : systemTimezone.value
89
+ },
44
90
  validationSchema: RELEASE_SCHEMA,
45
- children: ({ values, errors, handleChange }) => /* @__PURE__ */ jsxs(Form, { children: [
46
- /* @__PURE__ */ jsx(ModalBody, { children: /* @__PURE__ */ jsx(
47
- TextInput,
48
- {
49
- label: formatMessage({
50
- id: "content-releases.modal.form.input.label.release-name",
51
- defaultMessage: "Name"
52
- }),
53
- name: "name",
54
- value: values.name,
55
- error: errors.name,
56
- onChange: handleChange,
57
- required: true
58
- }
59
- ) }),
91
+ validateOnChange: false,
92
+ children: ({ values, errors, handleChange, setFieldValue }) => /* @__PURE__ */ jsxs(Form, { children: [
93
+ /* @__PURE__ */ jsx(ModalBody, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 6, children: [
94
+ /* @__PURE__ */ jsx(
95
+ TextInput,
96
+ {
97
+ label: formatMessage({
98
+ id: "content-releases.modal.form.input.label.release-name",
99
+ defaultMessage: "Name"
100
+ }),
101
+ name: "name",
102
+ value: values.name,
103
+ error: errors.name,
104
+ onChange: handleChange,
105
+ required: true
106
+ }
107
+ ),
108
+ IsSchedulingEnabled && /* @__PURE__ */ jsxs(Fragment, { children: [
109
+ /* @__PURE__ */ jsx(Box, { width: "max-content", children: /* @__PURE__ */ jsx(
110
+ Checkbox,
111
+ {
112
+ name: "isScheduled",
113
+ value: values.isScheduled,
114
+ onChange: (event) => {
115
+ setFieldValue("isScheduled", event.target.checked);
116
+ if (!event.target.checked) {
117
+ setFieldValue("date", null);
118
+ setFieldValue("time", "");
119
+ setFieldValue("timezone", null);
120
+ } else {
121
+ setFieldValue("date", initialValues.date);
122
+ setFieldValue("time", initialValues.time);
123
+ setFieldValue(
124
+ "timezone",
125
+ initialValues.timezone ?? systemTimezone?.value
126
+ );
127
+ }
128
+ },
129
+ children: /* @__PURE__ */ jsx(
130
+ Typography,
131
+ {
132
+ textColor: values.isScheduled ? "primary600" : "neutral800",
133
+ fontWeight: values.isScheduled ? "semiBold" : "regular",
134
+ children: formatMessage({
135
+ id: "modal.form.input.label.schedule-release",
136
+ defaultMessage: "Schedule release"
137
+ })
138
+ }
139
+ )
140
+ }
141
+ ) }),
142
+ values.isScheduled && /* @__PURE__ */ jsxs(Fragment, { children: [
143
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, alignItems: "start", children: [
144
+ /* @__PURE__ */ jsx(Box, { width: "100%", children: /* @__PURE__ */ jsx(
145
+ DatePicker,
146
+ {
147
+ label: formatMessage({
148
+ id: "content-releases.modal.form.input.label.date",
149
+ defaultMessage: "Date"
150
+ }),
151
+ name: "date",
152
+ error: errors.date,
153
+ onChange: (date) => {
154
+ const isoFormatDate = date ? formatISO(date, { representation: "date" }) : null;
155
+ setFieldValue("date", isoFormatDate);
156
+ },
157
+ clearLabel: formatMessage({
158
+ id: "content-releases.modal.form.input.clearLabel",
159
+ defaultMessage: "Clear"
160
+ }),
161
+ onClear: () => {
162
+ setFieldValue("date", null);
163
+ },
164
+ selectedDate: values.date || void 0,
165
+ required: true
166
+ }
167
+ ) }),
168
+ /* @__PURE__ */ jsx(Box, { width: "100%", children: /* @__PURE__ */ jsx(
169
+ TimePicker,
170
+ {
171
+ label: formatMessage({
172
+ id: "content-releases.modal.form.input.label.time",
173
+ defaultMessage: "Time"
174
+ }),
175
+ name: "time",
176
+ error: errors.time,
177
+ onChange: (time) => {
178
+ setFieldValue("time", time);
179
+ },
180
+ clearLabel: formatMessage({
181
+ id: "content-releases.modal.form.input.clearLabel",
182
+ defaultMessage: "Clear"
183
+ }),
184
+ onClear: () => {
185
+ setFieldValue("time", "");
186
+ },
187
+ value: values.time || void 0,
188
+ required: true
189
+ }
190
+ ) })
191
+ ] }),
192
+ /* @__PURE__ */ jsx(TimezoneComponent, { timezoneOptions: timezoneList })
193
+ ] })
194
+ ] })
195
+ ] }) }),
60
196
  /* @__PURE__ */ jsx(
61
197
  ModalFooter,
62
198
  {
63
199
  startActions: /* @__PURE__ */ jsx(Button, { onClick: handleClose, variant: "tertiary", name: "cancel", children: formatMessage({ id: "cancel", defaultMessage: "Cancel" }) }),
64
- endActions: /* @__PURE__ */ jsx(
65
- Button,
200
+ endActions: /* @__PURE__ */ jsx(Button, { name: "submit", loading: isLoading, type: "submit", children: formatMessage(
66
201
  {
67
- name: "submit",
68
- loading: isLoading,
69
- disabled: !values.name || values.name === initialValues.name,
70
- type: "submit",
71
- children: formatMessage(
72
- {
73
- id: "content-releases.modal.form.button.submit",
74
- defaultMessage: "{isCreatingRelease, select, true {Continue} other {Save}}"
75
- },
76
- { isCreatingRelease }
77
- )
78
- }
79
- )
202
+ id: "content-releases.modal.form.button.submit",
203
+ defaultMessage: "{isCreatingRelease, select, true {Continue} other {Save}}"
204
+ },
205
+ { isCreatingRelease }
206
+ ) })
80
207
  }
81
208
  )
82
209
  ] })
@@ -84,16 +211,59 @@ const ReleaseModal = ({
84
211
  )
85
212
  ] });
86
213
  };
214
+ const getTimezones = (selectedDate) => {
215
+ const timezoneList = Intl.supportedValuesOf("timeZone").map((timezone) => {
216
+ const utcOffset = getTimezoneOffset(timezone, selectedDate);
217
+ return { offset: utcOffset, value: `${utcOffset}_${timezone}` };
218
+ });
219
+ const systemTimezone = timezoneList.find(
220
+ (timezone) => timezone.value.split("_")[1] === Intl.DateTimeFormat().resolvedOptions().timeZone
221
+ );
222
+ return { timezoneList, systemTimezone };
223
+ };
224
+ const TimezoneComponent = ({ timezoneOptions }) => {
225
+ const { values, errors, setFieldValue } = useFormikContext();
226
+ const { formatMessage } = useIntl();
227
+ const [timezoneList, setTimezoneList] = React.useState(timezoneOptions);
228
+ React.useEffect(() => {
229
+ if (values.date) {
230
+ const { timezoneList: timezoneList2 } = getTimezones(new Date(values.date));
231
+ setTimezoneList(timezoneList2);
232
+ const updatedTimezone = values.timezone && timezoneList2.find((tz) => tz.value.split("_")[1] === values.timezone.split("_")[1]);
233
+ if (updatedTimezone) {
234
+ setFieldValue("timezone", updatedTimezone.value);
235
+ }
236
+ }
237
+ }, [setFieldValue, values.date, values.timezone]);
238
+ return /* @__PURE__ */ jsx(
239
+ Combobox,
240
+ {
241
+ label: formatMessage({
242
+ id: "content-releases.modal.form.input.label.timezone",
243
+ defaultMessage: "Timezone"
244
+ }),
245
+ name: "timezone",
246
+ value: values.timezone || void 0,
247
+ textValue: values.timezone ? values.timezone.replace("_", " ") : void 0,
248
+ onChange: (timezone) => {
249
+ setFieldValue("timezone", timezone);
250
+ },
251
+ onClear: () => {
252
+ setFieldValue("timezone", "");
253
+ },
254
+ error: errors.timezone,
255
+ required: true,
256
+ children: timezoneList.map((timezone) => /* @__PURE__ */ jsx(ComboboxOption, { value: timezone.value, children: timezone.value.replace("_", " ") }, timezone.value))
257
+ }
258
+ );
259
+ };
87
260
  const ReleaseInfoWrapper = styled(Flex)`
88
261
  align-self: stretch;
89
262
  border-bottom-right-radius: ${({ theme }) => theme.borderRadius};
90
263
  border-bottom-left-radius: ${({ theme }) => theme.borderRadius};
91
264
  border-top: 1px solid ${({ theme }) => theme.colors.neutral150};
92
265
  `;
93
- const StyledFlex = styled(Flex)`
94
- align-self: stretch;
95
- cursor: ${({ disabled }) => disabled ? "not-allowed" : "pointer"};
96
-
266
+ const StyledMenuItem = styled(Menu.Item)`
97
267
  svg path {
98
268
  fill: ${({ theme, disabled }) => disabled && theme.colors.neutral500};
99
269
  }
@@ -102,15 +272,15 @@ const StyledFlex = styled(Flex)`
102
272
  }
103
273
  `;
104
274
  const PencilIcon = styled(Pencil)`
105
- width: ${({ theme }) => theme.spaces[4]};
106
- height: ${({ theme }) => theme.spaces[4]};
275
+ width: ${({ theme }) => theme.spaces[3]};
276
+ height: ${({ theme }) => theme.spaces[3]};
107
277
  path {
108
278
  fill: ${({ theme }) => theme.colors.neutral600};
109
279
  }
110
280
  `;
111
281
  const TrashIcon = styled(Trash)`
112
- width: ${({ theme }) => theme.spaces[4]};
113
- height: ${({ theme }) => theme.spaces[4]};
282
+ width: ${({ theme }) => theme.spaces[3]};
283
+ height: ${({ theme }) => theme.spaces[3]};
114
284
  path {
115
285
  fill: ${({ theme }) => theme.colors.danger600};
116
286
  }
@@ -118,24 +288,6 @@ const TrashIcon = styled(Trash)`
118
288
  const TypographyMaxWidth = styled(Typography)`
119
289
  max-width: 300px;
120
290
  `;
121
- const PopoverButton = ({ onClick, disabled, children }) => {
122
- return /* @__PURE__ */ jsx(
123
- StyledFlex,
124
- {
125
- paddingTop: 2,
126
- paddingBottom: 2,
127
- paddingLeft: 4,
128
- paddingRight: 4,
129
- alignItems: "center",
130
- gap: 2,
131
- as: "button",
132
- hasRadius: true,
133
- onClick,
134
- disabled,
135
- children
136
- }
137
- );
138
- };
139
291
  const EntryValidationText = ({ action, schema, components, entry }) => {
140
292
  const { formatMessage } = useIntl();
141
293
  const { validate } = unstable_useDocument();
@@ -184,10 +336,8 @@ const ReleaseDetailsLayout = ({
184
336
  toggleWarningSubmit,
185
337
  children
186
338
  }) => {
187
- const { formatMessage } = useIntl();
339
+ const { formatMessage, formatDate, formatTime } = useIntl();
188
340
  const { releaseId } = useParams();
189
- const [isPopoverVisible, setIsPopoverVisible] = React.useState(false);
190
- const moreButtonRef = React.useRef(null);
191
341
  const {
192
342
  data,
193
343
  isLoading: isLoadingDetails,
@@ -201,14 +351,8 @@ const ReleaseDetailsLayout = ({
201
351
  allowedActions: { canUpdate, canDelete }
202
352
  } = useRBAC(PERMISSIONS);
203
353
  const dispatch = useTypedDispatch();
354
+ const { trackUsage } = useTracking();
204
355
  const release = data?.data;
205
- const handleTogglePopover = () => {
206
- setIsPopoverVisible((prev) => !prev);
207
- };
208
- const openReleaseModal = () => {
209
- toggleEditReleaseModal();
210
- handleTogglePopover();
211
- };
212
356
  const handlePublishRelease = async () => {
213
357
  const response = await publishRelease({ id: releaseId });
214
358
  if ("data" in response) {
@@ -219,6 +363,12 @@ const ReleaseDetailsLayout = ({
219
363
  defaultMessage: "Release was published successfully."
220
364
  })
221
365
  });
366
+ const { totalEntries: totalEntries2, totalPublishedEntries, totalUnpublishedEntries } = response.data.meta;
367
+ trackUsage("didPublishRelease", {
368
+ totalEntries: totalEntries2,
369
+ totalPublishedEntries,
370
+ totalUnpublishedEntries
371
+ });
222
372
  } else if (isAxiosError(response.error)) {
223
373
  toggleNotification({
224
374
  type: "warning",
@@ -231,13 +381,21 @@ const ReleaseDetailsLayout = ({
231
381
  });
232
382
  }
233
383
  };
234
- const openWarningConfirmDialog = () => {
235
- toggleWarningSubmit();
236
- handleTogglePopover();
237
- };
238
384
  const handleRefresh = () => {
239
385
  dispatch(releaseApi.util.invalidateTags([{ type: "ReleaseAction", id: "LIST" }]));
240
386
  };
387
+ const getCreatedByUser = () => {
388
+ if (!release?.createdBy) {
389
+ return null;
390
+ }
391
+ if (release.createdBy.username) {
392
+ return release.createdBy.username;
393
+ }
394
+ if (release.createdBy.firstname) {
395
+ return `${release.createdBy.firstname} ${release.createdBy.lastname || ""}`.trim();
396
+ }
397
+ return release.createdBy.email;
398
+ };
241
399
  if (isLoadingDetails) {
242
400
  return /* @__PURE__ */ jsx(Main, { "aria-busy": isLoadingDetails, children: /* @__PURE__ */ jsx(LoadingIndicatorPage, {}) });
243
401
  }
@@ -259,90 +417,139 @@ const ReleaseDetailsLayout = ({
259
417
  );
260
418
  }
261
419
  const totalEntries = release.actions.meta.count || 0;
262
- const createdBy = release.createdBy.lastname ? `${release.createdBy.firstname} ${release.createdBy.lastname}` : `${release.createdBy.firstname}`;
420
+ const hasCreatedByUser = Boolean(getCreatedByUser());
421
+ const IsSchedulingEnabled = window.strapi.future.isEnabled("contentReleasesScheduling");
422
+ const isScheduled = release.scheduledAt && release.timezone;
423
+ const numberOfEntriesText = formatMessage(
424
+ {
425
+ id: "content-releases.pages.Details.header-subtitle",
426
+ defaultMessage: "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
427
+ },
428
+ { number: totalEntries }
429
+ );
430
+ const scheduledText = isScheduled ? formatMessage(
431
+ {
432
+ id: "content-releases.pages.ReleaseDetails.header-subtitle.scheduled",
433
+ defaultMessage: "Scheduled for {date} at {time} ({offset})"
434
+ },
435
+ {
436
+ date: formatDate(new Date(release.scheduledAt), {
437
+ weekday: "long",
438
+ day: "numeric",
439
+ month: "long",
440
+ year: "numeric",
441
+ timeZone: release.timezone
442
+ }),
443
+ time: formatTime(new Date(release.scheduledAt), {
444
+ timeZone: release.timezone,
445
+ hourCycle: "h23"
446
+ }),
447
+ offset: getTimezoneOffset(release.timezone, new Date(release.scheduledAt))
448
+ }
449
+ ) : "";
263
450
  return /* @__PURE__ */ jsxs(Main, { "aria-busy": isLoadingDetails, children: [
264
451
  /* @__PURE__ */ jsx(
265
452
  HeaderLayout,
266
453
  {
267
454
  title: release.name,
268
- subtitle: formatMessage(
269
- {
270
- id: "content-releases.pages.Details.header-subtitle",
271
- defaultMessage: "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
272
- },
273
- { number: totalEntries }
274
- ),
455
+ subtitle: numberOfEntriesText + (IsSchedulingEnabled && isScheduled ? ` - ${scheduledText}` : ""),
275
456
  navigationAction: /* @__PURE__ */ jsx(Link, { startIcon: /* @__PURE__ */ jsx(ArrowLeft, {}), to: "/plugins/content-releases", children: formatMessage({
276
457
  id: "global.back",
277
458
  defaultMessage: "Back"
278
459
  }) }),
279
460
  primaryAction: !release.releasedAt && /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
280
- /* @__PURE__ */ jsx(
281
- IconButton,
282
- {
283
- label: formatMessage({
284
- id: "content-releases.header.actions.open-release-actions",
285
- defaultMessage: "Release actions"
286
- }),
287
- ref: moreButtonRef,
288
- onClick: handleTogglePopover,
289
- children: /* @__PURE__ */ jsx(More, {})
290
- }
291
- ),
292
- isPopoverVisible && /* @__PURE__ */ jsxs(
293
- Popover,
294
- {
295
- source: moreButtonRef,
296
- placement: "bottom-end",
297
- onDismiss: handleTogglePopover,
298
- spacing: 4,
299
- minWidth: "242px",
300
- children: [
301
- /* @__PURE__ */ jsxs(Flex, { alignItems: "center", justifyContent: "center", direction: "column", padding: 1, children: [
302
- /* @__PURE__ */ jsxs(PopoverButton, { disabled: !canUpdate, onClick: openReleaseModal, children: [
303
- /* @__PURE__ */ jsx(PencilIcon, {}),
304
- /* @__PURE__ */ jsx(Typography, { ellipsis: true, children: formatMessage({
305
- id: "content-releases.header.actions.edit",
306
- defaultMessage: "Edit"
307
- }) })
308
- ] }),
309
- /* @__PURE__ */ jsxs(PopoverButton, { disabled: !canDelete, onClick: openWarningConfirmDialog, children: [
310
- /* @__PURE__ */ jsx(TrashIcon, {}),
311
- /* @__PURE__ */ jsx(Typography, { ellipsis: true, textColor: "danger600", children: formatMessage({
312
- id: "content-releases.header.actions.delete",
313
- defaultMessage: "Delete"
314
- }) })
315
- ] })
316
- ] }),
317
- /* @__PURE__ */ jsxs(
318
- ReleaseInfoWrapper,
319
- {
320
- direction: "column",
321
- justifyContent: "center",
322
- alignItems: "flex-start",
323
- gap: 1,
324
- padding: 5,
325
- children: [
326
- /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: formatMessage({
327
- id: "content-releases.header.actions.created",
328
- defaultMessage: "Created"
329
- }) }),
330
- /* @__PURE__ */ jsxs(Typography, { variant: "pi", color: "neutral300", children: [
331
- /* @__PURE__ */ jsx(RelativeTime, { timestamp: new Date(release.createdAt) }),
332
- formatMessage(
333
- {
334
- id: "content-releases.header.actions.created.description",
335
- defaultMessage: " by {createdBy}"
336
- },
337
- { createdBy }
338
- )
339
- ] })
340
- ]
341
- }
342
- )
343
- ]
344
- }
345
- ),
461
+ /* @__PURE__ */ jsxs(Menu.Root, { children: [
462
+ /* @__PURE__ */ jsx(
463
+ Menu.Trigger,
464
+ {
465
+ as: IconButton,
466
+ paddingLeft: 2,
467
+ paddingRight: 2,
468
+ "aria-label": formatMessage({
469
+ id: "content-releases.header.actions.open-release-actions",
470
+ defaultMessage: "Release edit and delete menu"
471
+ }),
472
+ icon: /* @__PURE__ */ jsx(More, {}),
473
+ variant: "tertiary"
474
+ }
475
+ ),
476
+ /* @__PURE__ */ jsxs(Menu.Content, { top: 1, popoverPlacement: "bottom-end", children: [
477
+ /* @__PURE__ */ jsxs(
478
+ Flex,
479
+ {
480
+ alignItems: "center",
481
+ justifyContent: "center",
482
+ direction: "column",
483
+ padding: 1,
484
+ width: "100%",
485
+ children: [
486
+ /* @__PURE__ */ jsx(StyledMenuItem, { disabled: !canUpdate, onSelect: toggleEditReleaseModal, children: /* @__PURE__ */ jsxs(
487
+ Flex,
488
+ {
489
+ paddingTop: 2,
490
+ paddingBottom: 2,
491
+ alignItems: "center",
492
+ gap: 2,
493
+ hasRadius: true,
494
+ width: "100%",
495
+ children: [
496
+ /* @__PURE__ */ jsx(PencilIcon, {}),
497
+ /* @__PURE__ */ jsx(Typography, { ellipsis: true, children: formatMessage({
498
+ id: "content-releases.header.actions.edit",
499
+ defaultMessage: "Edit"
500
+ }) })
501
+ ]
502
+ }
503
+ ) }),
504
+ /* @__PURE__ */ jsx(StyledMenuItem, { disabled: !canDelete, onSelect: toggleWarningSubmit, children: /* @__PURE__ */ jsxs(
505
+ Flex,
506
+ {
507
+ paddingTop: 2,
508
+ paddingBottom: 2,
509
+ alignItems: "center",
510
+ gap: 2,
511
+ hasRadius: true,
512
+ width: "100%",
513
+ children: [
514
+ /* @__PURE__ */ jsx(TrashIcon, {}),
515
+ /* @__PURE__ */ jsx(Typography, { ellipsis: true, textColor: "danger600", children: formatMessage({
516
+ id: "content-releases.header.actions.delete",
517
+ defaultMessage: "Delete"
518
+ }) })
519
+ ]
520
+ }
521
+ ) })
522
+ ]
523
+ }
524
+ ),
525
+ /* @__PURE__ */ jsxs(
526
+ ReleaseInfoWrapper,
527
+ {
528
+ direction: "column",
529
+ justifyContent: "center",
530
+ alignItems: "flex-start",
531
+ gap: 1,
532
+ padding: 5,
533
+ children: [
534
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: formatMessage({
535
+ id: "content-releases.header.actions.created",
536
+ defaultMessage: "Created"
537
+ }) }),
538
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", color: "neutral300", children: [
539
+ /* @__PURE__ */ jsx(RelativeTime, { timestamp: new Date(release.createdAt) }),
540
+ formatMessage(
541
+ {
542
+ id: "content-releases.header.actions.created.description",
543
+ defaultMessage: "{hasCreatedByUser, select, true { by {createdBy}} other { by deleted user}}"
544
+ },
545
+ { createdBy: getCreatedByUser(), hasCreatedByUser }
546
+ )
547
+ ] })
548
+ ]
549
+ }
550
+ )
551
+ ] })
552
+ ] }),
346
553
  /* @__PURE__ */ jsx(Button, { size: "S", variant: "tertiary", onClick: handleRefresh, children: formatMessage({
347
554
  id: "content-releases.header.actions.refresh",
348
555
  defaultMessage: "Refresh"
@@ -398,6 +605,9 @@ const ReleaseDetailsBody = () => {
398
605
  isError: isReleaseError,
399
606
  error: releaseError
400
607
  } = useGetReleaseQuery({ id: releaseId });
608
+ const {
609
+ allowedActions: { canUpdate }
610
+ } = useRBAC(PERMISSIONS);
401
611
  const release = releaseData?.data;
402
612
  const selectedGroupBy = query?.groupBy || "contentType";
403
613
  const {
@@ -504,7 +714,7 @@ const ReleaseDetailsBody = () => {
504
714
  SingleSelect,
505
715
  {
506
716
  "aria-label": formatMessage({
507
- id: "content-releases.pages.ReleaseDetails.groupBy.label",
717
+ id: "content-releases.pages.ReleaseDetails.groupBy.aria-label",
508
718
  defaultMessage: "Group by"
509
719
  }),
510
720
  customizeContent: (value) => formatMessage(
@@ -522,7 +732,7 @@ const ReleaseDetailsBody = () => {
522
732
  }
523
733
  ) }),
524
734
  Object.keys(releaseActions).map((key) => /* @__PURE__ */ jsxs(Flex, { gap: 4, direction: "column", alignItems: "stretch", children: [
525
- /* @__PURE__ */ jsx(Flex, { children: /* @__PURE__ */ jsx(Badge, { children: key }) }),
735
+ /* @__PURE__ */ jsx(Flex, { role: "separator", "aria-label": key, children: /* @__PURE__ */ jsx(Badge, { children: key }) }),
526
736
  /* @__PURE__ */ jsx(
527
737
  Table.Root,
528
738
  {
@@ -611,7 +821,8 @@ const ReleaseDetailsBody = () => {
611
821
  {
612
822
  selected: type,
613
823
  handleChange: (e) => handleChangeType(e, id, [key, actionIndex]),
614
- name: `release-action-${id}-type`
824
+ name: `release-action-${id}-type`,
825
+ disabled: !canUpdate
615
826
  }
616
827
  ) }),
617
828
  !release.releasedAt && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -690,11 +901,18 @@ const ReleaseDetailsPage = () => {
690
901
  }
691
902
  );
692
903
  }
693
- const title = isSuccessDetails && data?.data?.name || "";
904
+ const releaseData = isSuccessDetails && data?.data || null;
905
+ const title = releaseData?.name || "";
906
+ const timezone = releaseData?.timezone ?? null;
907
+ const scheduledAt = releaseData?.scheduledAt && timezone ? utcToZonedTime(releaseData.scheduledAt, timezone) : null;
908
+ const date = scheduledAt ? new Date(format(scheduledAt, "yyyy-MM-dd")) : null;
909
+ const time = scheduledAt ? format(scheduledAt, "HH:mm") : "";
694
910
  const handleEditRelease = async (values) => {
695
911
  const response = await updateRelease({
696
912
  id: releaseId,
697
- name: values.name
913
+ name: values.name,
914
+ scheduledAt: values.scheduledAt,
915
+ timezone: values.timezone
698
916
  });
699
917
  if ("data" in response) {
700
918
  toggleNotification({
@@ -748,7 +966,14 @@ const ReleaseDetailsPage = () => {
748
966
  handleClose: toggleEditReleaseModal,
749
967
  handleSubmit: handleEditRelease,
750
968
  isLoading: isLoadingDetails || isSubmittingForm,
751
- initialValues: { name: title || "" }
969
+ initialValues: {
970
+ name: title || "",
971
+ scheduledAt,
972
+ date,
973
+ time,
974
+ isScheduled: Boolean(scheduledAt),
975
+ timezone
976
+ }
752
977
  }
753
978
  ),
754
979
  /* @__PURE__ */ jsx(
@@ -773,6 +998,7 @@ const LinkCard = styled(Link$2)`
773
998
  `;
774
999
  const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }) => {
775
1000
  const { formatMessage } = useIntl();
1001
+ const IsSchedulingEnabled = window.strapi.future.isEnabled("contentReleasesScheduling");
776
1002
  if (isError) {
777
1003
  return /* @__PURE__ */ jsx(AnErrorOccurred, {});
778
1004
  }
@@ -793,7 +1019,7 @@ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }) => {
793
1019
  }
794
1020
  );
795
1021
  }
796
- return /* @__PURE__ */ jsx(Grid, { gap: 4, children: releases.map(({ id, name, actions }) => /* @__PURE__ */ jsx(GridItem, { col: 3, s: 6, xs: 12, children: /* @__PURE__ */ jsx(LinkCard, { href: `content-releases/${id}`, isExternal: false, children: /* @__PURE__ */ jsxs(
1022
+ return /* @__PURE__ */ jsx(Grid, { gap: 4, children: releases.map(({ id, name, actions, scheduledAt }) => /* @__PURE__ */ jsx(GridItem, { col: 3, s: 6, xs: 12, children: /* @__PURE__ */ jsx(LinkCard, { href: `content-releases/${id}`, isExternal: false, children: /* @__PURE__ */ jsxs(
797
1023
  Flex,
798
1024
  {
799
1025
  direction: "column",
@@ -808,7 +1034,10 @@ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }) => {
808
1034
  gap: 2,
809
1035
  children: [
810
1036
  /* @__PURE__ */ jsx(Typography, { as: "h3", variant: "delta", fontWeight: "bold", children: name }),
811
- /* @__PURE__ */ jsx(Typography, { variant: "pi", children: formatMessage(
1037
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: IsSchedulingEnabled ? scheduledAt ? /* @__PURE__ */ jsx(RelativeTime, { timestamp: new Date(scheduledAt) }) : formatMessage({
1038
+ id: "content-releases.pages.Releases.not-scheduled",
1039
+ defaultMessage: "Not scheduled"
1040
+ }) : formatMessage(
812
1041
  {
813
1042
  id: "content-releases.page.Releases.release-item.entries",
814
1043
  defaultMessage: "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
@@ -828,7 +1057,13 @@ const StyledAlert = styled(Alert)`
828
1057
  }
829
1058
  `;
830
1059
  const INITIAL_FORM_VALUES = {
831
- name: ""
1060
+ name: "",
1061
+ date: null,
1062
+ time: "",
1063
+ // Remove future flag check after Scheduling Beta release and replace with true as creating new release should include scheduling by default
1064
+ isScheduled: window.strapi.future.isEnabled("contentReleasesScheduling"),
1065
+ scheduledAt: null,
1066
+ timezone: null
832
1067
  };
833
1068
  const ReleasesPage = () => {
834
1069
  const tabRef = React.useRef(null);
@@ -842,7 +1077,8 @@ const ReleasesPage = () => {
842
1077
  const response = useGetReleasesQuery(query);
843
1078
  const [createRelease, { isLoading: isSubmittingForm }] = useCreateReleaseMutation();
844
1079
  const { getFeature } = useLicenseLimits();
845
- const { maximumNumberOfPendingReleases = 3 } = getFeature("cms-content-releases");
1080
+ const { maximumReleases = 3 } = getFeature("cms-content-releases");
1081
+ const { trackUsage } = useTracking();
846
1082
  const { isLoading, isSuccess, isError } = response;
847
1083
  const activeTab = response?.currentData?.meta?.activeTab || "pending";
848
1084
  const activeTabIndex = ["pending", "done"].indexOf(activeTab);
@@ -874,7 +1110,7 @@ const ReleasesPage = () => {
874
1110
  return /* @__PURE__ */ jsx(Main, { "aria-busy": isLoading, children: /* @__PURE__ */ jsx(LoadingIndicatorPage, {}) });
875
1111
  }
876
1112
  const totalReleases = isSuccess && response.currentData?.meta?.pagination?.total || 0;
877
- const hasReachedMaximumPendingReleases = totalReleases >= maximumNumberOfPendingReleases;
1113
+ const hasReachedMaximumPendingReleases = totalReleases >= maximumReleases;
878
1114
  const handleTabChange = (index) => {
879
1115
  setQuery({
880
1116
  ...query,
@@ -887,9 +1123,11 @@ const ReleasesPage = () => {
887
1123
  }
888
1124
  });
889
1125
  };
890
- const handleAddRelease = async (values) => {
1126
+ const handleAddRelease = async ({ name, scheduledAt, timezone }) => {
891
1127
  const response2 = await createRelease({
892
- name: values.name
1128
+ name,
1129
+ scheduledAt,
1130
+ timezone
893
1131
  });
894
1132
  if ("data" in response2) {
895
1133
  toggleNotification({
@@ -899,6 +1137,7 @@ const ReleasesPage = () => {
899
1137
  defaultMessage: "Release created."
900
1138
  })
901
1139
  });
1140
+ trackUsage("didCreateRelease");
902
1141
  push(`/plugins/content-releases/${response2.data.data.id}`);
903
1142
  } else if (isAxiosError(response2.error)) {
904
1143
  toggleNotification({
@@ -955,7 +1194,7 @@ const ReleasesPage = () => {
955
1194
  id: "content-releases.pages.Releases.max-limit-reached.title",
956
1195
  defaultMessage: "You have reached the {number} pending {number, plural, one {release} other {releases}} limit."
957
1196
  },
958
- { number: maximumNumberOfPendingReleases }
1197
+ { number: maximumReleases }
959
1198
  ),
960
1199
  onClose: () => {
961
1200
  },
@@ -1050,4 +1289,4 @@ const App = () => {
1050
1289
  export {
1051
1290
  App
1052
1291
  };
1053
- //# sourceMappingURL=App-_Jj3tWts.mjs.map
1292
+ //# sourceMappingURL=App-xAkiD42p.mjs.map