@strapi/content-releases 4.20.1 → 4.20.3

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.
@@ -1,15 +1,18 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
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-gkExFBa0.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-rEfNT9PC.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, SingleSelect, SingleSelectOption, Badge, Tr, Td, Icon, Tooltip, Alert, TabGroup, Box, Tabs, Tab, Divider, TabPanels, TabPanel, EmptyStateLayout, Grid, GridItem } from "@strapi/design-system";
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
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";
@@ -17,11 +20,21 @@ import "@reduxjs/toolkit/query/react";
17
20
  import "react-redux";
18
21
  const RELEASE_SCHEMA = yup.object().shape({
19
22
  name: yup.string().trim().required(),
20
- // scheduledAt is a date, but we always receive strings from the client
21
23
  scheduledAt: yup.string().nullable(),
22
- timezone: yup.string().when("scheduledAt", {
23
- is: (scheduledAt) => !!scheduledAt,
24
- then: yup.string().required(),
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(),
25
38
  otherwise: yup.string().nullable()
26
39
  })
27
40
  }).required().noUnknown();
@@ -34,6 +47,24 @@ const ReleaseModal = ({
34
47
  const { formatMessage } = useIntl();
35
48
  const { pathname } = useLocation();
36
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
+ };
37
68
  return /* @__PURE__ */ jsxs(ModalLayout, { onClose: handleClose, labelledBy: "title", children: [
38
69
  /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(Typography, { id: "title", fontWeight: "bold", textColor: "neutral800", children: formatMessage(
39
70
  {
@@ -45,45 +76,134 @@ const ReleaseModal = ({
45
76
  /* @__PURE__ */ jsx(
46
77
  Formik,
47
78
  {
48
- validateOnChange: false,
49
- onSubmit: handleSubmit,
50
- 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
+ },
51
90
  validationSchema: RELEASE_SCHEMA,
52
- children: ({ values, errors, handleChange }) => /* @__PURE__ */ jsxs(Form, { children: [
53
- /* @__PURE__ */ jsx(ModalBody, { children: /* @__PURE__ */ jsx(
54
- TextInput,
55
- {
56
- label: formatMessage({
57
- id: "content-releases.modal.form.input.label.release-name",
58
- defaultMessage: "Name"
59
- }),
60
- name: "name",
61
- value: values.name,
62
- error: errors.name,
63
- onChange: handleChange,
64
- required: true
65
- }
66
- ) }),
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
+ ] }) }),
67
196
  /* @__PURE__ */ jsx(
68
197
  ModalFooter,
69
198
  {
70
199
  startActions: /* @__PURE__ */ jsx(Button, { onClick: handleClose, variant: "tertiary", name: "cancel", children: formatMessage({ id: "cancel", defaultMessage: "Cancel" }) }),
71
- endActions: /* @__PURE__ */ jsx(
72
- Button,
200
+ endActions: /* @__PURE__ */ jsx(Button, { name: "submit", loading: isLoading, type: "submit", children: formatMessage(
73
201
  {
74
- name: "submit",
75
- loading: isLoading,
76
- disabled: !values.name || values.name === initialValues.name,
77
- type: "submit",
78
- children: formatMessage(
79
- {
80
- id: "content-releases.modal.form.button.submit",
81
- defaultMessage: "{isCreatingRelease, select, true {Continue} other {Save}}"
82
- },
83
- { isCreatingRelease }
84
- )
85
- }
86
- )
202
+ id: "content-releases.modal.form.button.submit",
203
+ defaultMessage: "{isCreatingRelease, select, true {Continue} other {Save}}"
204
+ },
205
+ { isCreatingRelease }
206
+ ) })
87
207
  }
88
208
  )
89
209
  ] })
@@ -91,6 +211,52 @@ const ReleaseModal = ({
91
211
  )
92
212
  ] });
93
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
+ };
94
260
  const ReleaseInfoWrapper = styled(Flex)`
95
261
  align-self: stretch;
96
262
  border-bottom-right-radius: ${({ theme }) => theme.borderRadius};
@@ -170,7 +336,7 @@ const ReleaseDetailsLayout = ({
170
336
  toggleWarningSubmit,
171
337
  children
172
338
  }) => {
173
- const { formatMessage } = useIntl();
339
+ const { formatMessage, formatDate, formatTime } = useIntl();
174
340
  const { releaseId } = useParams();
175
341
  const {
176
342
  data,
@@ -252,18 +418,41 @@ const ReleaseDetailsLayout = ({
252
418
  }
253
419
  const totalEntries = release.actions.meta.count || 0;
254
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
+ ) : "";
255
450
  return /* @__PURE__ */ jsxs(Main, { "aria-busy": isLoadingDetails, children: [
256
451
  /* @__PURE__ */ jsx(
257
452
  HeaderLayout,
258
453
  {
259
454
  title: release.name,
260
- subtitle: formatMessage(
261
- {
262
- id: "content-releases.pages.Details.header-subtitle",
263
- defaultMessage: "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
264
- },
265
- { number: totalEntries }
266
- ),
455
+ subtitle: numberOfEntriesText + (IsSchedulingEnabled && isScheduled ? ` - ${scheduledText}` : ""),
267
456
  navigationAction: /* @__PURE__ */ jsx(Link, { startIcon: /* @__PURE__ */ jsx(ArrowLeft, {}), to: "/plugins/content-releases", children: formatMessage({
268
457
  id: "global.back",
269
458
  defaultMessage: "Back"
@@ -712,11 +901,18 @@ const ReleaseDetailsPage = () => {
712
901
  }
713
902
  );
714
903
  }
715
- 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") : "";
716
910
  const handleEditRelease = async (values) => {
717
911
  const response = await updateRelease({
718
912
  id: releaseId,
719
- name: values.name
913
+ name: values.name,
914
+ scheduledAt: values.scheduledAt,
915
+ timezone: values.timezone
720
916
  });
721
917
  if ("data" in response) {
722
918
  toggleNotification({
@@ -770,7 +966,14 @@ const ReleaseDetailsPage = () => {
770
966
  handleClose: toggleEditReleaseModal,
771
967
  handleSubmit: handleEditRelease,
772
968
  isLoading: isLoadingDetails || isSubmittingForm,
773
- initialValues: { name: title || "" }
969
+ initialValues: {
970
+ name: title || "",
971
+ scheduledAt,
972
+ date,
973
+ time,
974
+ isScheduled: Boolean(scheduledAt),
975
+ timezone
976
+ }
774
977
  }
775
978
  ),
776
979
  /* @__PURE__ */ jsx(
@@ -795,6 +998,7 @@ const LinkCard = styled(Link$2)`
795
998
  `;
796
999
  const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }) => {
797
1000
  const { formatMessage } = useIntl();
1001
+ const IsSchedulingEnabled = window.strapi.future.isEnabled("contentReleasesScheduling");
798
1002
  if (isError) {
799
1003
  return /* @__PURE__ */ jsx(AnErrorOccurred, {});
800
1004
  }
@@ -815,7 +1019,7 @@ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }) => {
815
1019
  }
816
1020
  );
817
1021
  }
818
- 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(
819
1023
  Flex,
820
1024
  {
821
1025
  direction: "column",
@@ -830,7 +1034,10 @@ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }) => {
830
1034
  gap: 2,
831
1035
  children: [
832
1036
  /* @__PURE__ */ jsx(Typography, { as: "h3", variant: "delta", fontWeight: "bold", children: name }),
833
- /* @__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(
834
1041
  {
835
1042
  id: "content-releases.page.Releases.release-item.entries",
836
1043
  defaultMessage: "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
@@ -850,7 +1057,13 @@ const StyledAlert = styled(Alert)`
850
1057
  }
851
1058
  `;
852
1059
  const INITIAL_FORM_VALUES = {
853
- 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
854
1067
  };
855
1068
  const ReleasesPage = () => {
856
1069
  const tabRef = React.useRef(null);
@@ -910,9 +1123,11 @@ const ReleasesPage = () => {
910
1123
  }
911
1124
  });
912
1125
  };
913
- const handleAddRelease = async (values) => {
1126
+ const handleAddRelease = async ({ name, scheduledAt, timezone }) => {
914
1127
  const response2 = await createRelease({
915
- name: values.name
1128
+ name,
1129
+ scheduledAt,
1130
+ timezone
916
1131
  });
917
1132
  if ("data" in response2) {
918
1133
  toggleNotification({
@@ -1074,4 +1289,4 @@ const App = () => {
1074
1289
  export {
1075
1290
  App
1076
1291
  };
1077
- //# sourceMappingURL=App-U6GbyLIE.mjs.map
1292
+ //# sourceMappingURL=App-hWBsb1nt.mjs.map