@strapi/content-releases 0.0.0-next.c5f067b5650921187770124e9b6c8186e805e242 → 0.0.0-next.cfecf3ad808761571ce11e528113e5c1ea5f87fd

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