@strapi/content-releases 0.0.0-next.6d59515520a3850456f256fb0e4c54b75054ddf4 → 0.0.0-next.78ea7925e0dad75936ae2e937a041a0666e3d65a

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-L1jSxCiL.mjs → App-bpzO2Ljh.mjs} +762 -454
  2. package/dist/_chunks/App-bpzO2Ljh.mjs.map +1 -0
  3. package/dist/_chunks/{App-_20W9dYa.js → App-p8aKBitd.js} +754 -445
  4. package/dist/_chunks/App-p8aKBitd.js.map +1 -0
  5. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs +51 -0
  6. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +1 -0
  7. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js +51 -0
  8. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +1 -0
  9. package/dist/_chunks/{en-MyLPoISH.mjs → en-WuuhP6Bn.mjs} +21 -6
  10. package/dist/_chunks/en-WuuhP6Bn.mjs.map +1 -0
  11. package/dist/_chunks/{en-gYDqKYFd.js → en-gcJJ5htG.js} +21 -6
  12. package/dist/_chunks/en-gcJJ5htG.js.map +1 -0
  13. package/dist/_chunks/{index-c4zRX_sg.mjs → index-AECgcaDa.mjs} +171 -45
  14. package/dist/_chunks/index-AECgcaDa.mjs.map +1 -0
  15. package/dist/_chunks/{index-KJa1Rb5F.js → index-fP3qoWZ4.js} +159 -33
  16. package/dist/_chunks/index-fP3qoWZ4.js.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 +952 -397
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +951 -397
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +14 -11
  24. package/dist/_chunks/App-L1jSxCiL.mjs.map +0 -1
  25. package/dist/_chunks/App-_20W9dYa.js.map +0 -1
  26. package/dist/_chunks/en-MyLPoISH.mjs.map +0 -1
  27. package/dist/_chunks/en-gYDqKYFd.js.map +0 -1
  28. package/dist/_chunks/index-KJa1Rb5F.js.map +0 -1
  29. package/dist/_chunks/index-c4zRX_sg.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";
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-c4zRX_sg.mjs";
2
+ import { RelativeTime, useNotification, useAPIErrorHandler, useQueryParams, useTracking, LoadingIndicatorPage, CheckPermissions, PageSizeURLQuery, PaginationURLQuery, AnErrorOccurred, ConfirmDialog, useRBAC, 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-AECgcaDa.mjs";
5
5
  import * as React from "react";
6
- import { unstable_useDocument } 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, 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, 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,135 @@ 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
+ minDate: utcToZonedTime(/* @__PURE__ */ new Date(), values.timezone.split("&")[1])
167
+ }
168
+ ) }),
169
+ /* @__PURE__ */ jsx(Box, { width: "100%", children: /* @__PURE__ */ jsx(
170
+ TimePicker,
171
+ {
172
+ label: formatMessage({
173
+ id: "content-releases.modal.form.input.label.time",
174
+ defaultMessage: "Time"
175
+ }),
176
+ name: "time",
177
+ error: errors.time,
178
+ onChange: (time) => {
179
+ setFieldValue("time", time);
180
+ },
181
+ clearLabel: formatMessage({
182
+ id: "content-releases.modal.form.input.clearLabel",
183
+ defaultMessage: "Clear"
184
+ }),
185
+ onClear: () => {
186
+ setFieldValue("time", "");
187
+ },
188
+ value: values.time || void 0,
189
+ required: true
190
+ }
191
+ ) })
192
+ ] }),
193
+ /* @__PURE__ */ jsx(TimezoneComponent, { timezoneOptions: timezoneList })
194
+ ] })
195
+ ] })
196
+ ] }) }),
60
197
  /* @__PURE__ */ jsx(
61
198
  ModalFooter,
62
199
  {
63
200
  startActions: /* @__PURE__ */ jsx(Button, { onClick: handleClose, variant: "tertiary", name: "cancel", children: formatMessage({ id: "cancel", defaultMessage: "Cancel" }) }),
64
- endActions: /* @__PURE__ */ jsx(
65
- Button,
201
+ endActions: /* @__PURE__ */ jsx(Button, { name: "submit", loading: isLoading, type: "submit", children: formatMessage(
66
202
  {
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
- )
203
+ id: "content-releases.modal.form.button.submit",
204
+ defaultMessage: "{isCreatingRelease, select, true {Continue} other {Save}}"
205
+ },
206
+ { isCreatingRelease }
207
+ ) })
80
208
  }
81
209
  )
82
210
  ] })
@@ -84,33 +212,404 @@ const ReleaseModal = ({
84
212
  )
85
213
  ] });
86
214
  };
215
+ const getTimezones = (selectedDate) => {
216
+ const timezoneList = Intl.supportedValuesOf("timeZone").map((timezone) => {
217
+ const utcOffset = getTimezoneOffset(timezone, selectedDate);
218
+ return { offset: utcOffset, value: `${utcOffset}&${timezone}` };
219
+ });
220
+ const systemTimezone = timezoneList.find(
221
+ (timezone) => timezone.value.split("&")[1] === Intl.DateTimeFormat().resolvedOptions().timeZone
222
+ );
223
+ return { timezoneList, systemTimezone };
224
+ };
225
+ const TimezoneComponent = ({ timezoneOptions }) => {
226
+ const { values, errors, setFieldValue } = useFormikContext();
227
+ const { formatMessage } = useIntl();
228
+ const [timezoneList, setTimezoneList] = React.useState(timezoneOptions);
229
+ React.useEffect(() => {
230
+ if (values.date) {
231
+ const { timezoneList: timezoneList2 } = getTimezones(new Date(values.date));
232
+ setTimezoneList(timezoneList2);
233
+ const updatedTimezone = values.timezone && timezoneList2.find((tz) => tz.value.split("&")[1] === values.timezone.split("&")[1]);
234
+ if (updatedTimezone) {
235
+ setFieldValue("timezone", updatedTimezone.value);
236
+ }
237
+ }
238
+ }, [setFieldValue, values.date, values.timezone]);
239
+ return /* @__PURE__ */ jsx(
240
+ Combobox,
241
+ {
242
+ label: formatMessage({
243
+ id: "content-releases.modal.form.input.label.timezone",
244
+ defaultMessage: "Timezone"
245
+ }),
246
+ autocomplete: { type: "list", filter: "contains" },
247
+ name: "timezone",
248
+ value: values.timezone || void 0,
249
+ textValue: values.timezone ? values.timezone.replace(/&/, " ") : void 0,
250
+ onChange: (timezone) => {
251
+ setFieldValue("timezone", timezone);
252
+ },
253
+ onTextValueChange: (timezone) => {
254
+ setFieldValue("timezone", timezone);
255
+ },
256
+ onClear: () => {
257
+ setFieldValue("timezone", "");
258
+ },
259
+ error: errors.timezone,
260
+ required: true,
261
+ children: timezoneList.map((timezone) => /* @__PURE__ */ jsx(ComboboxOption, { value: timezone.value, children: timezone.value.replace(/&/, " ") }, timezone.value))
262
+ }
263
+ );
264
+ };
265
+ const LinkCard = styled(Link)`
266
+ display: block;
267
+ `;
268
+ const CapitalizeRelativeTime = styled(RelativeTime)`
269
+ text-transform: capitalize;
270
+ `;
271
+ const getBadgeProps = (status) => {
272
+ let color;
273
+ switch (status) {
274
+ case "ready":
275
+ color = "success";
276
+ break;
277
+ case "blocked":
278
+ color = "warning";
279
+ break;
280
+ case "failed":
281
+ color = "danger";
282
+ break;
283
+ case "done":
284
+ color = "primary";
285
+ break;
286
+ case "empty":
287
+ default:
288
+ color = "neutral";
289
+ }
290
+ return {
291
+ textColor: `${color}600`,
292
+ backgroundColor: `${color}100`,
293
+ borderColor: `${color}200`
294
+ };
295
+ };
296
+ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }) => {
297
+ const { formatMessage } = useIntl();
298
+ const IsSchedulingEnabled = window.strapi.future.isEnabled("contentReleasesScheduling");
299
+ if (isError) {
300
+ return /* @__PURE__ */ jsx(AnErrorOccurred, {});
301
+ }
302
+ if (releases?.length === 0) {
303
+ return /* @__PURE__ */ jsx(
304
+ EmptyStateLayout,
305
+ {
306
+ content: formatMessage(
307
+ {
308
+ id: "content-releases.page.Releases.tab.emptyEntries",
309
+ defaultMessage: "No releases"
310
+ },
311
+ {
312
+ target: sectionTitle
313
+ }
314
+ ),
315
+ icon: /* @__PURE__ */ jsx(EmptyDocuments, { width: "10rem" })
316
+ }
317
+ );
318
+ }
319
+ return /* @__PURE__ */ jsx(Grid, { gap: 4, children: releases.map(({ id, name, actions, scheduledAt, status }) => /* @__PURE__ */ jsx(GridItem, { col: 3, s: 6, xs: 12, children: /* @__PURE__ */ jsx(LinkCard, { href: `content-releases/${id}`, isExternal: false, children: /* @__PURE__ */ jsxs(
320
+ Flex,
321
+ {
322
+ direction: "column",
323
+ justifyContent: "space-between",
324
+ padding: 4,
325
+ hasRadius: true,
326
+ background: "neutral0",
327
+ shadow: "tableShadow",
328
+ height: "100%",
329
+ width: "100%",
330
+ alignItems: "start",
331
+ gap: 4,
332
+ children: [
333
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "start", gap: 1, children: [
334
+ /* @__PURE__ */ jsx(Typography, { as: "h3", variant: "delta", fontWeight: "bold", children: name }),
335
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: IsSchedulingEnabled ? scheduledAt ? /* @__PURE__ */ jsx(CapitalizeRelativeTime, { timestamp: new Date(scheduledAt) }) : formatMessage({
336
+ id: "content-releases.pages.Releases.not-scheduled",
337
+ defaultMessage: "Not scheduled"
338
+ }) : formatMessage(
339
+ {
340
+ id: "content-releases.page.Releases.release-item.entries",
341
+ defaultMessage: "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
342
+ },
343
+ { number: actions.meta.count }
344
+ ) })
345
+ ] }),
346
+ /* @__PURE__ */ jsx(Badge, { ...getBadgeProps(status), children: status })
347
+ ]
348
+ }
349
+ ) }) }, id)) });
350
+ };
351
+ const StyledAlert = styled(Alert)`
352
+ button {
353
+ display: none;
354
+ }
355
+ p + div {
356
+ margin-left: auto;
357
+ }
358
+ `;
359
+ const INITIAL_FORM_VALUES = {
360
+ name: "",
361
+ date: null,
362
+ time: "",
363
+ // Remove future flag check after Scheduling Beta release and replace with true as creating new release should include scheduling by default
364
+ isScheduled: window.strapi.future.isEnabled("contentReleasesScheduling"),
365
+ scheduledAt: null,
366
+ timezone: null
367
+ };
368
+ const ReleasesPage = () => {
369
+ const tabRef = React.useRef(null);
370
+ const location = useLocation();
371
+ const [releaseModalShown, setReleaseModalShown] = React.useState(false);
372
+ const toggleNotification = useNotification();
373
+ const { formatMessage } = useIntl();
374
+ const { push, replace } = useHistory();
375
+ const { formatAPIError } = useAPIErrorHandler();
376
+ const [{ query }, setQuery] = useQueryParams();
377
+ const response = useGetReleasesQuery(query);
378
+ const [createRelease, { isLoading: isSubmittingForm }] = useCreateReleaseMutation();
379
+ const { getFeature } = useLicenseLimits();
380
+ const { maximumReleases = 3 } = getFeature("cms-content-releases");
381
+ const { trackUsage } = useTracking();
382
+ const { isLoading, isSuccess, isError } = response;
383
+ const activeTab = response?.currentData?.meta?.activeTab || "pending";
384
+ const activeTabIndex = ["pending", "done"].indexOf(activeTab);
385
+ React.useEffect(() => {
386
+ if (location?.state?.errors) {
387
+ toggleNotification({
388
+ type: "warning",
389
+ title: formatMessage({
390
+ id: "content-releases.pages.Releases.notification.error.title",
391
+ defaultMessage: "Your request could not be processed."
392
+ }),
393
+ message: formatMessage({
394
+ id: "content-releases.pages.Releases.notification.error.message",
395
+ defaultMessage: "Please try again or open another release."
396
+ })
397
+ });
398
+ replace({ state: null });
399
+ }
400
+ }, [formatMessage, location?.state?.errors, replace, toggleNotification]);
401
+ React.useEffect(() => {
402
+ if (tabRef.current) {
403
+ tabRef.current._handlers.setSelectedTabIndex(activeTabIndex);
404
+ }
405
+ }, [activeTabIndex]);
406
+ const toggleAddReleaseModal = () => {
407
+ setReleaseModalShown((prev) => !prev);
408
+ };
409
+ if (isLoading) {
410
+ return /* @__PURE__ */ jsx(Main, { "aria-busy": isLoading, children: /* @__PURE__ */ jsx(LoadingIndicatorPage, {}) });
411
+ }
412
+ const totalPendingReleases = isSuccess && response.currentData?.meta?.pendingReleasesCount || 0;
413
+ const hasReachedMaximumPendingReleases = totalPendingReleases >= maximumReleases;
414
+ const handleTabChange = (index) => {
415
+ setQuery({
416
+ ...query,
417
+ page: 1,
418
+ pageSize: response?.currentData?.meta?.pagination?.pageSize || 16,
419
+ filters: {
420
+ releasedAt: {
421
+ $notNull: index === 0 ? false : true
422
+ }
423
+ }
424
+ });
425
+ };
426
+ const handleAddRelease = async ({ name, scheduledAt, timezone }) => {
427
+ const response2 = await createRelease({
428
+ name,
429
+ scheduledAt,
430
+ timezone
431
+ });
432
+ if ("data" in response2) {
433
+ toggleNotification({
434
+ type: "success",
435
+ message: formatMessage({
436
+ id: "content-releases.modal.release-created-notification-success",
437
+ defaultMessage: "Release created."
438
+ })
439
+ });
440
+ trackUsage("didCreateRelease");
441
+ push(`/plugins/content-releases/${response2.data.data.id}`);
442
+ } else if (isAxiosError(response2.error)) {
443
+ toggleNotification({
444
+ type: "warning",
445
+ message: formatAPIError(response2.error)
446
+ });
447
+ } else {
448
+ toggleNotification({
449
+ type: "warning",
450
+ message: formatMessage({ id: "notification.error", defaultMessage: "An error occurred" })
451
+ });
452
+ }
453
+ };
454
+ return /* @__PURE__ */ jsxs(Main, { "aria-busy": isLoading, children: [
455
+ /* @__PURE__ */ jsx(
456
+ HeaderLayout,
457
+ {
458
+ title: formatMessage({
459
+ id: "content-releases.pages.Releases.title",
460
+ defaultMessage: "Releases"
461
+ }),
462
+ subtitle: formatMessage({
463
+ id: "content-releases.pages.Releases.header-subtitle",
464
+ defaultMessage: "Create and manage content updates"
465
+ }),
466
+ primaryAction: /* @__PURE__ */ jsx(CheckPermissions, { permissions: PERMISSIONS.create, children: /* @__PURE__ */ jsx(
467
+ Button,
468
+ {
469
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
470
+ onClick: toggleAddReleaseModal,
471
+ disabled: hasReachedMaximumPendingReleases,
472
+ children: formatMessage({
473
+ id: "content-releases.header.actions.add-release",
474
+ defaultMessage: "New release"
475
+ })
476
+ }
477
+ ) })
478
+ }
479
+ ),
480
+ /* @__PURE__ */ jsx(ContentLayout, { children: /* @__PURE__ */ jsxs(Fragment, { children: [
481
+ hasReachedMaximumPendingReleases && /* @__PURE__ */ jsx(
482
+ StyledAlert,
483
+ {
484
+ marginBottom: 6,
485
+ action: /* @__PURE__ */ jsx(Link, { href: "https://strapi.io/pricing-cloud", isExternal: true, children: formatMessage({
486
+ id: "content-releases.pages.Releases.max-limit-reached.action",
487
+ defaultMessage: "Explore plans"
488
+ }) }),
489
+ title: formatMessage(
490
+ {
491
+ id: "content-releases.pages.Releases.max-limit-reached.title",
492
+ defaultMessage: "You have reached the {number} pending {number, plural, one {release} other {releases}} limit."
493
+ },
494
+ { number: maximumReleases }
495
+ ),
496
+ onClose: () => {
497
+ },
498
+ closeLabel: "",
499
+ children: formatMessage({
500
+ id: "content-releases.pages.Releases.max-limit-reached.message",
501
+ defaultMessage: "Upgrade to manage an unlimited number of releases."
502
+ })
503
+ }
504
+ ),
505
+ /* @__PURE__ */ jsxs(
506
+ TabGroup,
507
+ {
508
+ label: formatMessage({
509
+ id: "content-releases.pages.Releases.tab-group.label",
510
+ defaultMessage: "Releases list"
511
+ }),
512
+ variant: "simple",
513
+ initialSelectedTabIndex: activeTabIndex,
514
+ onTabChange: handleTabChange,
515
+ ref: tabRef,
516
+ children: [
517
+ /* @__PURE__ */ jsxs(Box, { paddingBottom: 8, children: [
518
+ /* @__PURE__ */ jsxs(Tabs, { children: [
519
+ /* @__PURE__ */ jsx(Tab, { children: formatMessage(
520
+ {
521
+ id: "content-releases.pages.Releases.tab.pending",
522
+ defaultMessage: "Pending ({count})"
523
+ },
524
+ {
525
+ count: totalPendingReleases
526
+ }
527
+ ) }),
528
+ /* @__PURE__ */ jsx(Tab, { children: formatMessage({
529
+ id: "content-releases.pages.Releases.tab.done",
530
+ defaultMessage: "Done"
531
+ }) })
532
+ ] }),
533
+ /* @__PURE__ */ jsx(Divider, {})
534
+ ] }),
535
+ /* @__PURE__ */ jsxs(TabPanels, { children: [
536
+ /* @__PURE__ */ jsx(TabPanel, { children: /* @__PURE__ */ jsx(
537
+ ReleasesGrid,
538
+ {
539
+ sectionTitle: "pending",
540
+ releases: response?.currentData?.data,
541
+ isError
542
+ }
543
+ ) }),
544
+ /* @__PURE__ */ jsx(TabPanel, { children: /* @__PURE__ */ jsx(
545
+ ReleasesGrid,
546
+ {
547
+ sectionTitle: "done",
548
+ releases: response?.currentData?.data,
549
+ isError
550
+ }
551
+ ) })
552
+ ] })
553
+ ]
554
+ }
555
+ ),
556
+ response.currentData?.meta?.pagination?.total ? /* @__PURE__ */ jsxs(Flex, { paddingTop: 4, alignItems: "flex-end", justifyContent: "space-between", children: [
557
+ /* @__PURE__ */ jsx(
558
+ PageSizeURLQuery,
559
+ {
560
+ options: ["8", "16", "32", "64"],
561
+ defaultValue: response?.currentData?.meta?.pagination?.pageSize.toString()
562
+ }
563
+ ),
564
+ /* @__PURE__ */ jsx(
565
+ PaginationURLQuery,
566
+ {
567
+ pagination: {
568
+ pageCount: response?.currentData?.meta?.pagination?.pageCount || 0
569
+ }
570
+ }
571
+ )
572
+ ] }) : null
573
+ ] }) }),
574
+ releaseModalShown && /* @__PURE__ */ jsx(
575
+ ReleaseModal,
576
+ {
577
+ handleClose: toggleAddReleaseModal,
578
+ handleSubmit: handleAddRelease,
579
+ isLoading: isSubmittingForm,
580
+ initialValues: INITIAL_FORM_VALUES
581
+ }
582
+ )
583
+ ] });
584
+ };
87
585
  const ReleaseInfoWrapper = styled(Flex)`
88
586
  align-self: stretch;
89
587
  border-bottom-right-radius: ${({ theme }) => theme.borderRadius};
90
588
  border-bottom-left-radius: ${({ theme }) => theme.borderRadius};
91
589
  border-top: 1px solid ${({ theme }) => theme.colors.neutral150};
92
590
  `;
93
- const StyledFlex = styled(Flex)`
94
- align-self: stretch;
95
- cursor: ${({ disabled }) => disabled ? "not-allowed" : "pointer"};
96
-
591
+ const StyledMenuItem = styled(Menu.Item)`
97
592
  svg path {
98
593
  fill: ${({ theme, disabled }) => disabled && theme.colors.neutral500};
99
594
  }
100
595
  span {
101
596
  color: ${({ theme, disabled }) => disabled && theme.colors.neutral500};
102
597
  }
598
+
599
+ &:hover {
600
+ background: ${({ theme, variant = "neutral" }) => theme.colors[`${variant}100`]};
601
+ }
103
602
  `;
104
603
  const PencilIcon = styled(Pencil)`
105
- width: ${({ theme }) => theme.spaces[4]};
106
- height: ${({ theme }) => theme.spaces[4]};
604
+ width: ${({ theme }) => theme.spaces[3]};
605
+ height: ${({ theme }) => theme.spaces[3]};
107
606
  path {
108
607
  fill: ${({ theme }) => theme.colors.neutral600};
109
608
  }
110
609
  `;
111
610
  const TrashIcon = styled(Trash)`
112
- width: ${({ theme }) => theme.spaces[4]};
113
- height: ${({ theme }) => theme.spaces[4]};
611
+ width: ${({ theme }) => theme.spaces[3]};
612
+ height: ${({ theme }) => theme.spaces[3]};
114
613
  path {
115
614
  fill: ${({ theme }) => theme.colors.danger600};
116
615
  }
@@ -118,24 +617,6 @@ const TrashIcon = styled(Trash)`
118
617
  const TypographyMaxWidth = styled(Typography)`
119
618
  max-width: 300px;
120
619
  `;
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
620
  const EntryValidationText = ({ action, schema, components, entry }) => {
140
621
  const { formatMessage } = useIntl();
141
622
  const { validate } = unstable_useDocument();
@@ -184,10 +665,8 @@ const ReleaseDetailsLayout = ({
184
665
  toggleWarningSubmit,
185
666
  children
186
667
  }) => {
187
- const { formatMessage } = useIntl();
668
+ const { formatMessage, formatDate, formatTime } = useIntl();
188
669
  const { releaseId } = useParams();
189
- const [isPopoverVisible, setIsPopoverVisible] = React.useState(false);
190
- const moreButtonRef = React.useRef(null);
191
670
  const {
192
671
  data,
193
672
  isLoading: isLoadingDetails,
@@ -201,14 +680,8 @@ const ReleaseDetailsLayout = ({
201
680
  allowedActions: { canUpdate, canDelete }
202
681
  } = useRBAC(PERMISSIONS);
203
682
  const dispatch = useTypedDispatch();
683
+ const { trackUsage } = useTracking();
204
684
  const release = data?.data;
205
- const handleTogglePopover = () => {
206
- setIsPopoverVisible((prev) => !prev);
207
- };
208
- const openReleaseModal = () => {
209
- toggleEditReleaseModal();
210
- handleTogglePopover();
211
- };
212
685
  const handlePublishRelease = async () => {
213
686
  const response = await publishRelease({ id: releaseId });
214
687
  if ("data" in response) {
@@ -219,6 +692,12 @@ const ReleaseDetailsLayout = ({
219
692
  defaultMessage: "Release was published successfully."
220
693
  })
221
694
  });
695
+ const { totalEntries: totalEntries2, totalPublishedEntries, totalUnpublishedEntries } = response.data.meta;
696
+ trackUsage("didPublishRelease", {
697
+ totalEntries: totalEntries2,
698
+ totalPublishedEntries,
699
+ totalUnpublishedEntries
700
+ });
222
701
  } else if (isAxiosError(response.error)) {
223
702
  toggleNotification({
224
703
  type: "warning",
@@ -231,13 +710,21 @@ const ReleaseDetailsLayout = ({
231
710
  });
232
711
  }
233
712
  };
234
- const openWarningConfirmDialog = () => {
235
- toggleWarningSubmit();
236
- handleTogglePopover();
237
- };
238
713
  const handleRefresh = () => {
239
714
  dispatch(releaseApi.util.invalidateTags([{ type: "ReleaseAction", id: "LIST" }]));
240
715
  };
716
+ const getCreatedByUser = () => {
717
+ if (!release?.createdBy) {
718
+ return null;
719
+ }
720
+ if (release.createdBy.username) {
721
+ return release.createdBy.username;
722
+ }
723
+ if (release.createdBy.firstname) {
724
+ return `${release.createdBy.firstname} ${release.createdBy.lastname || ""}`.trim();
725
+ }
726
+ return release.createdBy.email;
727
+ };
241
728
  if (isLoadingDetails) {
242
729
  return /* @__PURE__ */ jsx(Main, { "aria-busy": isLoadingDetails, children: /* @__PURE__ */ jsx(LoadingIndicatorPage, {}) });
243
730
  }
@@ -259,90 +746,128 @@ const ReleaseDetailsLayout = ({
259
746
  );
260
747
  }
261
748
  const totalEntries = release.actions.meta.count || 0;
262
- const createdBy = release.createdBy.lastname ? `${release.createdBy.firstname} ${release.createdBy.lastname}` : `${release.createdBy.firstname}`;
749
+ const hasCreatedByUser = Boolean(getCreatedByUser());
750
+ const IsSchedulingEnabled = window.strapi.future.isEnabled("contentReleasesScheduling");
751
+ const isScheduled = release.scheduledAt && release.timezone;
752
+ const numberOfEntriesText = formatMessage(
753
+ {
754
+ id: "content-releases.pages.Details.header-subtitle",
755
+ defaultMessage: "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
756
+ },
757
+ { number: totalEntries }
758
+ );
759
+ const scheduledText = isScheduled ? formatMessage(
760
+ {
761
+ id: "content-releases.pages.ReleaseDetails.header-subtitle.scheduled",
762
+ defaultMessage: "Scheduled for {date} at {time} ({offset})"
763
+ },
764
+ {
765
+ date: formatDate(new Date(release.scheduledAt), {
766
+ weekday: "long",
767
+ day: "numeric",
768
+ month: "long",
769
+ year: "numeric",
770
+ timeZone: release.timezone
771
+ }),
772
+ time: formatTime(new Date(release.scheduledAt), {
773
+ timeZone: release.timezone,
774
+ hourCycle: "h23"
775
+ }),
776
+ offset: getTimezoneOffset(release.timezone, new Date(release.scheduledAt))
777
+ }
778
+ ) : "";
263
779
  return /* @__PURE__ */ jsxs(Main, { "aria-busy": isLoadingDetails, children: [
264
780
  /* @__PURE__ */ jsx(
265
781
  HeaderLayout,
266
782
  {
267
783
  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
- ),
275
- navigationAction: /* @__PURE__ */ jsx(Link, { startIcon: /* @__PURE__ */ jsx(ArrowLeft, {}), to: "/plugins/content-releases", children: formatMessage({
784
+ subtitle: /* @__PURE__ */ jsxs(Flex, { gap: 2, lineHeight: 6, children: [
785
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", variant: "epsilon", children: numberOfEntriesText + (IsSchedulingEnabled && isScheduled ? ` - ${scheduledText}` : "") }),
786
+ /* @__PURE__ */ jsx(Badge, { ...getBadgeProps(release.status), children: release.status })
787
+ ] }),
788
+ navigationAction: /* @__PURE__ */ jsx(Link$1, { startIcon: /* @__PURE__ */ jsx(ArrowLeft, {}), to: "/plugins/content-releases", children: formatMessage({
276
789
  id: "global.back",
277
790
  defaultMessage: "Back"
278
791
  }) }),
279
792
  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
- ),
793
+ /* @__PURE__ */ jsxs(Menu.Root, { children: [
794
+ /* @__PURE__ */ jsx(
795
+ Menu.Trigger,
796
+ {
797
+ as: IconButton,
798
+ paddingLeft: 2,
799
+ paddingRight: 2,
800
+ "aria-label": formatMessage({
801
+ id: "content-releases.header.actions.open-release-actions",
802
+ defaultMessage: "Release edit and delete menu"
803
+ }),
804
+ icon: /* @__PURE__ */ jsx(More, {}),
805
+ variant: "tertiary"
806
+ }
807
+ ),
808
+ /* @__PURE__ */ jsxs(Menu.Content, { top: 1, popoverPlacement: "bottom-end", children: [
809
+ /* @__PURE__ */ jsxs(
810
+ Flex,
811
+ {
812
+ alignItems: "center",
813
+ justifyContent: "center",
814
+ direction: "column",
815
+ padding: 1,
816
+ width: "100%",
817
+ children: [
818
+ /* @__PURE__ */ jsx(StyledMenuItem, { disabled: !canUpdate, onSelect: toggleEditReleaseModal, children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, hasRadius: true, width: "100%", children: [
819
+ /* @__PURE__ */ jsx(PencilIcon, {}),
820
+ /* @__PURE__ */ jsx(Typography, { ellipsis: true, children: formatMessage({
821
+ id: "content-releases.header.actions.edit",
822
+ defaultMessage: "Edit"
823
+ }) })
824
+ ] }) }),
825
+ /* @__PURE__ */ jsx(
826
+ StyledMenuItem,
827
+ {
828
+ disabled: !canDelete,
829
+ onSelect: toggleWarningSubmit,
830
+ variant: "danger",
831
+ children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, hasRadius: true, width: "100%", children: [
832
+ /* @__PURE__ */ jsx(TrashIcon, {}),
833
+ /* @__PURE__ */ jsx(Typography, { ellipsis: true, textColor: "danger600", children: formatMessage({
834
+ id: "content-releases.header.actions.delete",
835
+ defaultMessage: "Delete"
836
+ }) })
837
+ ] })
838
+ }
839
+ )
840
+ ]
841
+ }
842
+ ),
843
+ /* @__PURE__ */ jsxs(
844
+ ReleaseInfoWrapper,
845
+ {
846
+ direction: "column",
847
+ justifyContent: "center",
848
+ alignItems: "flex-start",
849
+ gap: 1,
850
+ padding: 5,
851
+ children: [
852
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: formatMessage({
853
+ id: "content-releases.header.actions.created",
854
+ defaultMessage: "Created"
855
+ }) }),
856
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", color: "neutral300", children: [
857
+ /* @__PURE__ */ jsx(RelativeTime, { timestamp: new Date(release.createdAt) }),
858
+ formatMessage(
859
+ {
860
+ id: "content-releases.header.actions.created.description",
861
+ defaultMessage: "{hasCreatedByUser, select, true { by {createdBy}} other { by deleted user}}"
862
+ },
863
+ { createdBy: getCreatedByUser(), hasCreatedByUser }
864
+ )
865
+ ] })
866
+ ]
867
+ }
868
+ )
869
+ ] })
870
+ ] }),
346
871
  /* @__PURE__ */ jsx(Button, { size: "S", variant: "tertiary", onClick: handleRefresh, children: formatMessage({
347
872
  id: "content-releases.header.actions.refresh",
348
873
  defaultMessage: "Refresh"
@@ -398,6 +923,9 @@ const ReleaseDetailsBody = () => {
398
923
  isError: isReleaseError,
399
924
  error: releaseError
400
925
  } = useGetReleaseQuery({ id: releaseId });
926
+ const {
927
+ allowedActions: { canUpdate }
928
+ } = useRBAC(PERMISSIONS);
401
929
  const release = releaseData?.data;
402
930
  const selectedGroupBy = query?.groupBy || "contentType";
403
931
  const {
@@ -411,7 +939,7 @@ const ReleaseDetailsBody = () => {
411
939
  releaseId
412
940
  });
413
941
  const [updateReleaseAction] = useUpdateReleaseActionMutation();
414
- const handleChangeType = async (e, actionId) => {
942
+ const handleChangeType = async (e, actionId, actionPath) => {
415
943
  const response = await updateReleaseAction({
416
944
  params: {
417
945
  releaseId,
@@ -419,7 +947,11 @@ const ReleaseDetailsBody = () => {
419
947
  },
420
948
  body: {
421
949
  type: e.target.value
422
- }
950
+ },
951
+ query,
952
+ // We are passing the query params to make optimistic updates
953
+ actionPath
954
+ // We are passing the action path to found the position in the cache of the action for optimistic updates
423
955
  });
424
956
  if ("error" in response) {
425
957
  if (isAxiosError(response.error)) {
@@ -480,7 +1012,7 @@ const ReleaseDetailsBody = () => {
480
1012
  action: /* @__PURE__ */ jsx(
481
1013
  LinkButton,
482
1014
  {
483
- as: Link$1,
1015
+ as: Link$2,
484
1016
  to: {
485
1017
  pathname: "/content-manager"
486
1018
  },
@@ -500,7 +1032,7 @@ const ReleaseDetailsBody = () => {
500
1032
  SingleSelect,
501
1033
  {
502
1034
  "aria-label": formatMessage({
503
- id: "content-releases.pages.ReleaseDetails.groupBy.label",
1035
+ id: "content-releases.pages.ReleaseDetails.groupBy.aria-label",
504
1036
  defaultMessage: "Group by"
505
1037
  }),
506
1038
  customizeContent: (value) => formatMessage(
@@ -518,7 +1050,7 @@ const ReleaseDetailsBody = () => {
518
1050
  }
519
1051
  ) }),
520
1052
  Object.keys(releaseActions).map((key) => /* @__PURE__ */ jsxs(Flex, { gap: 4, direction: "column", alignItems: "stretch", children: [
521
- /* @__PURE__ */ jsx(Flex, { children: /* @__PURE__ */ jsx(Badge, { children: key }) }),
1053
+ /* @__PURE__ */ jsx(Flex, { role: "separator", "aria-label": key, children: /* @__PURE__ */ jsx(Badge, { children: key }) }),
522
1054
  /* @__PURE__ */ jsx(
523
1055
  Table.Root,
524
1056
  {
@@ -588,56 +1120,59 @@ const ReleaseDetailsBody = () => {
588
1120
  )
589
1121
  ] }),
590
1122
  /* @__PURE__ */ jsx(Table.LoadingBody, {}),
591
- /* @__PURE__ */ jsx(Table.Body, { children: releaseActions[key].map(({ id, contentType, locale, type, entry }) => /* @__PURE__ */ jsxs(Tr, { children: [
592
- /* @__PURE__ */ jsx(Td, { width: "25%", maxWidth: "200px", children: /* @__PURE__ */ jsx(Typography, { ellipsis: true, children: `${contentType.mainFieldValue || entry.id}` }) }),
593
- /* @__PURE__ */ jsx(Td, { width: "10%", children: /* @__PURE__ */ jsx(Typography, { children: `${locale?.name ? locale.name : "-"}` }) }),
594
- /* @__PURE__ */ jsx(Td, { width: "10%", children: /* @__PURE__ */ jsx(Typography, { children: contentType.displayName || "" }) }),
595
- /* @__PURE__ */ jsx(Td, { width: "20%", children: release.releasedAt ? /* @__PURE__ */ jsx(Typography, { children: formatMessage(
596
- {
597
- id: "content-releases.page.ReleaseDetails.table.action-published",
598
- defaultMessage: "This entry was <b>{isPublish, select, true {published} other {unpublished}}</b>."
599
- },
600
- {
601
- isPublish: type === "publish",
602
- b: (children) => /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children })
603
- }
604
- ) }) : /* @__PURE__ */ jsx(
605
- ReleaseActionOptions,
606
- {
607
- selected: type,
608
- handleChange: (e) => handleChangeType(e, id),
609
- name: `release-action-${id}-type`
610
- }
611
- ) }),
612
- !release.releasedAt && /* @__PURE__ */ jsxs(Fragment, { children: [
613
- /* @__PURE__ */ jsx(Td, { width: "20%", minWidth: "200px", children: /* @__PURE__ */ jsx(
614
- EntryValidationText,
1123
+ /* @__PURE__ */ jsx(Table.Body, { children: releaseActions[key].map(
1124
+ ({ id, contentType, locale, type, entry }, actionIndex) => /* @__PURE__ */ jsxs(Tr, { children: [
1125
+ /* @__PURE__ */ jsx(Td, { width: "25%", maxWidth: "200px", children: /* @__PURE__ */ jsx(Typography, { ellipsis: true, children: `${contentType.mainFieldValue || entry.id}` }) }),
1126
+ /* @__PURE__ */ jsx(Td, { width: "10%", children: /* @__PURE__ */ jsx(Typography, { children: `${locale?.name ? locale.name : "-"}` }) }),
1127
+ /* @__PURE__ */ jsx(Td, { width: "10%", children: /* @__PURE__ */ jsx(Typography, { children: contentType.displayName || "" }) }),
1128
+ /* @__PURE__ */ jsx(Td, { width: "20%", children: release.releasedAt ? /* @__PURE__ */ jsx(Typography, { children: formatMessage(
1129
+ {
1130
+ id: "content-releases.page.ReleaseDetails.table.action-published",
1131
+ defaultMessage: "This entry was <b>{isPublish, select, true {published} other {unpublished}}</b>."
1132
+ },
1133
+ {
1134
+ isPublish: type === "publish",
1135
+ b: (children) => /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children })
1136
+ }
1137
+ ) }) : /* @__PURE__ */ jsx(
1138
+ ReleaseActionOptions,
615
1139
  {
616
- action: type,
617
- schema: contentTypes?.[contentType.uid],
618
- components,
619
- entry
1140
+ selected: type,
1141
+ handleChange: (e) => handleChangeType(e, id, [key, actionIndex]),
1142
+ name: `release-action-${id}-type`,
1143
+ disabled: !canUpdate
620
1144
  }
621
1145
  ) }),
622
- /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(ReleaseActionMenu.Root, { children: [
623
- /* @__PURE__ */ jsx(
624
- ReleaseActionMenu.ReleaseActionEntryLinkItem,
625
- {
626
- contentTypeUid: contentType.uid,
627
- entryId: entry.id,
628
- locale: locale?.code
629
- }
630
- ),
631
- /* @__PURE__ */ jsx(
632
- ReleaseActionMenu.DeleteReleaseActionItem,
1146
+ !release.releasedAt && /* @__PURE__ */ jsxs(Fragment, { children: [
1147
+ /* @__PURE__ */ jsx(Td, { width: "20%", minWidth: "200px", children: /* @__PURE__ */ jsx(
1148
+ EntryValidationText,
633
1149
  {
634
- releaseId: release.id,
635
- actionId: id
1150
+ action: type,
1151
+ schema: contentTypes?.[contentType.uid],
1152
+ components,
1153
+ entry
636
1154
  }
637
- )
638
- ] }) }) })
639
- ] })
640
- ] }, id)) })
1155
+ ) }),
1156
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(ReleaseActionMenu.Root, { children: [
1157
+ /* @__PURE__ */ jsx(
1158
+ ReleaseActionMenu.ReleaseActionEntryLinkItem,
1159
+ {
1160
+ contentTypeUid: contentType.uid,
1161
+ entryId: entry.id,
1162
+ locale: locale?.code
1163
+ }
1164
+ ),
1165
+ /* @__PURE__ */ jsx(
1166
+ ReleaseActionMenu.DeleteReleaseActionItem,
1167
+ {
1168
+ releaseId: release.id,
1169
+ actionId: id
1170
+ }
1171
+ )
1172
+ ] }) }) })
1173
+ ] })
1174
+ ] }, id)
1175
+ ) })
641
1176
  ] })
642
1177
  }
643
1178
  )
@@ -660,7 +1195,7 @@ const ReleaseDetailsPage = () => {
660
1195
  const { releaseId } = useParams();
661
1196
  const toggleNotification = useNotification();
662
1197
  const { formatAPIError } = useAPIErrorHandler();
663
- const { push } = useHistory();
1198
+ const { replace } = useHistory();
664
1199
  const [releaseModalShown, setReleaseModalShown] = React.useState(false);
665
1200
  const [showWarningSubmit, setWarningSubmit] = React.useState(false);
666
1201
  const {
@@ -684,11 +1219,18 @@ const ReleaseDetailsPage = () => {
684
1219
  }
685
1220
  );
686
1221
  }
687
- const title = isSuccessDetails && data?.data?.name || "";
1222
+ const releaseData = isSuccessDetails && data?.data || null;
1223
+ const title = releaseData?.name || "";
1224
+ const timezone = releaseData?.timezone ?? null;
1225
+ const scheduledAt = releaseData?.scheduledAt && timezone ? utcToZonedTime(releaseData.scheduledAt, timezone) : null;
1226
+ const date = scheduledAt ? new Date(format(scheduledAt, "yyyy-MM-dd")) : null;
1227
+ const time = scheduledAt ? format(scheduledAt, "HH:mm") : "";
688
1228
  const handleEditRelease = async (values) => {
689
1229
  const response = await updateRelease({
690
1230
  id: releaseId,
691
- name: values.name
1231
+ name: values.name,
1232
+ scheduledAt: values.scheduledAt,
1233
+ timezone: values.timezone
692
1234
  });
693
1235
  if ("data" in response) {
694
1236
  toggleNotification({
@@ -716,7 +1258,7 @@ const ReleaseDetailsPage = () => {
716
1258
  id: releaseId
717
1259
  });
718
1260
  if ("data" in response) {
719
- push("/plugins/content-releases");
1261
+ replace("/plugins/content-releases");
720
1262
  } else if (isAxiosError(response.error)) {
721
1263
  toggleNotification({
722
1264
  type: "warning",
@@ -742,7 +1284,14 @@ const ReleaseDetailsPage = () => {
742
1284
  handleClose: toggleEditReleaseModal,
743
1285
  handleSubmit: handleEditRelease,
744
1286
  isLoading: isLoadingDetails || isSubmittingForm,
745
- initialValues: { name: title || "" }
1287
+ initialValues: {
1288
+ name: title || "",
1289
+ scheduledAt,
1290
+ date,
1291
+ time,
1292
+ isScheduled: Boolean(scheduledAt),
1293
+ timezone
1294
+ }
746
1295
  }
747
1296
  ),
748
1297
  /* @__PURE__ */ jsx(
@@ -762,247 +1311,6 @@ const ReleaseDetailsPage = () => {
762
1311
  }
763
1312
  );
764
1313
  };
765
- const ReleasesLayout = ({
766
- isLoading,
767
- totalReleases,
768
- onClickAddRelease,
769
- children
770
- }) => {
771
- const { formatMessage } = useIntl();
772
- return /* @__PURE__ */ jsxs(Main, { "aria-busy": isLoading, children: [
773
- /* @__PURE__ */ jsx(
774
- HeaderLayout,
775
- {
776
- title: formatMessage({
777
- id: "content-releases.pages.Releases.title",
778
- defaultMessage: "Releases"
779
- }),
780
- subtitle: !isLoading && formatMessage(
781
- {
782
- id: "content-releases.pages.Releases.header-subtitle",
783
- defaultMessage: "{number, plural, =0 {No releases} one {# release} other {# releases}}"
784
- },
785
- { number: totalReleases }
786
- ),
787
- primaryAction: /* @__PURE__ */ jsx(CheckPermissions, { permissions: PERMISSIONS.create, children: /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: onClickAddRelease, children: formatMessage({
788
- id: "content-releases.header.actions.add-release",
789
- defaultMessage: "New release"
790
- }) }) })
791
- }
792
- ),
793
- children
794
- ] });
795
- };
796
- const LinkCard = styled(Link$2)`
797
- display: block;
798
- `;
799
- const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }) => {
800
- const { formatMessage } = useIntl();
801
- if (isError) {
802
- return /* @__PURE__ */ jsx(AnErrorOccurred, {});
803
- }
804
- if (releases?.length === 0) {
805
- return /* @__PURE__ */ jsx(
806
- EmptyStateLayout,
807
- {
808
- content: formatMessage(
809
- {
810
- id: "content-releases.page.Releases.tab.emptyEntries",
811
- defaultMessage: "No releases"
812
- },
813
- {
814
- target: sectionTitle
815
- }
816
- ),
817
- icon: /* @__PURE__ */ jsx(EmptyDocuments, { width: "10rem" })
818
- }
819
- );
820
- }
821
- 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(
822
- Flex,
823
- {
824
- direction: "column",
825
- justifyContent: "space-between",
826
- padding: 4,
827
- hasRadius: true,
828
- background: "neutral0",
829
- shadow: "tableShadow",
830
- height: "100%",
831
- width: "100%",
832
- alignItems: "start",
833
- gap: 2,
834
- children: [
835
- /* @__PURE__ */ jsx(Typography, { as: "h3", variant: "delta", fontWeight: "bold", children: name }),
836
- /* @__PURE__ */ jsx(Typography, { variant: "pi", children: formatMessage(
837
- {
838
- id: "content-releases.page.Releases.release-item.entries",
839
- defaultMessage: "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
840
- },
841
- { number: actions.meta.count }
842
- ) })
843
- ]
844
- }
845
- ) }) }, id)) });
846
- };
847
- const INITIAL_FORM_VALUES = {
848
- name: ""
849
- };
850
- const ReleasesPage = () => {
851
- const tabRef = React.useRef(null);
852
- const location = useLocation();
853
- const [releaseModalShown, setReleaseModalShown] = React.useState(false);
854
- const toggleNotification = useNotification();
855
- const { formatMessage } = useIntl();
856
- const { push, replace } = useHistory();
857
- const { formatAPIError } = useAPIErrorHandler();
858
- const [{ query }, setQuery] = useQueryParams();
859
- const response = useGetReleasesQuery(query);
860
- const [createRelease, { isLoading: isSubmittingForm }] = useCreateReleaseMutation();
861
- const { isLoading, isSuccess, isError } = response;
862
- const activeTab = response?.currentData?.meta?.activeTab || "pending";
863
- const activeTabIndex = ["pending", "done"].indexOf(activeTab);
864
- React.useEffect(() => {
865
- if (location?.state?.errors) {
866
- toggleNotification({
867
- type: "warning",
868
- title: formatMessage({
869
- id: "content-releases.pages.Releases.notification.error.title",
870
- defaultMessage: "Your request could not be processed."
871
- }),
872
- message: formatMessage({
873
- id: "content-releases.pages.Releases.notification.error.message",
874
- defaultMessage: "Please try again or open another release."
875
- })
876
- });
877
- replace({ state: null });
878
- }
879
- }, [formatMessage, location?.state?.errors, replace, toggleNotification]);
880
- React.useEffect(() => {
881
- if (tabRef.current) {
882
- tabRef.current._handlers.setSelectedTabIndex(activeTabIndex);
883
- }
884
- }, [activeTabIndex]);
885
- const toggleAddReleaseModal = () => {
886
- setReleaseModalShown((prev) => !prev);
887
- };
888
- if (isLoading) {
889
- return /* @__PURE__ */ jsx(ReleasesLayout, { onClickAddRelease: toggleAddReleaseModal, isLoading: true, children: /* @__PURE__ */ jsx(ContentLayout, { children: /* @__PURE__ */ jsx(LoadingIndicatorPage, {}) }) });
890
- }
891
- const totalReleases = isSuccess && response.currentData?.meta?.pagination?.total || 0;
892
- const handleTabChange = (index) => {
893
- setQuery({
894
- ...query,
895
- page: 1,
896
- pageSize: response?.currentData?.meta?.pagination?.pageSize || 16,
897
- filters: {
898
- releasedAt: {
899
- $notNull: index === 0 ? false : true
900
- }
901
- }
902
- });
903
- };
904
- const handleAddRelease = async (values) => {
905
- const response2 = await createRelease({
906
- name: values.name
907
- });
908
- if ("data" in response2) {
909
- toggleNotification({
910
- type: "success",
911
- message: formatMessage({
912
- id: "content-releases.modal.release-created-notification-success",
913
- defaultMessage: "Release created."
914
- })
915
- });
916
- push(`/plugins/content-releases/${response2.data.data.id}`);
917
- } else if (isAxiosError(response2.error)) {
918
- toggleNotification({
919
- type: "warning",
920
- message: formatAPIError(response2.error)
921
- });
922
- } else {
923
- toggleNotification({
924
- type: "warning",
925
- message: formatMessage({ id: "notification.error", defaultMessage: "An error occurred" })
926
- });
927
- }
928
- };
929
- return /* @__PURE__ */ jsxs(ReleasesLayout, { onClickAddRelease: toggleAddReleaseModal, totalReleases, children: [
930
- /* @__PURE__ */ jsx(ContentLayout, { children: /* @__PURE__ */ jsxs(Fragment, { children: [
931
- /* @__PURE__ */ jsxs(
932
- TabGroup,
933
- {
934
- label: formatMessage({
935
- id: "content-releases.pages.Releases.tab-group.label",
936
- defaultMessage: "Releases list"
937
- }),
938
- variant: "simple",
939
- initialSelectedTabIndex: activeTabIndex,
940
- onTabChange: handleTabChange,
941
- ref: tabRef,
942
- children: [
943
- /* @__PURE__ */ jsxs(Box, { paddingBottom: 8, children: [
944
- /* @__PURE__ */ jsxs(Tabs, { children: [
945
- /* @__PURE__ */ jsx(Tab, { children: formatMessage({
946
- id: "content-releases.pages.Releases.tab.pending",
947
- defaultMessage: "Pending"
948
- }) }),
949
- /* @__PURE__ */ jsx(Tab, { children: formatMessage({
950
- id: "content-releases.pages.Releases.tab.done",
951
- defaultMessage: "Done"
952
- }) })
953
- ] }),
954
- /* @__PURE__ */ jsx(Divider, {})
955
- ] }),
956
- /* @__PURE__ */ jsxs(TabPanels, { children: [
957
- /* @__PURE__ */ jsx(TabPanel, { children: /* @__PURE__ */ jsx(
958
- ReleasesGrid,
959
- {
960
- sectionTitle: "pending",
961
- releases: response?.currentData?.data,
962
- isError
963
- }
964
- ) }),
965
- /* @__PURE__ */ jsx(TabPanel, { children: /* @__PURE__ */ jsx(
966
- ReleasesGrid,
967
- {
968
- sectionTitle: "done",
969
- releases: response?.currentData?.data,
970
- isError
971
- }
972
- ) })
973
- ] })
974
- ]
975
- }
976
- ),
977
- totalReleases > 0 && /* @__PURE__ */ jsxs(Flex, { paddingTop: 4, alignItems: "flex-end", justifyContent: "space-between", children: [
978
- /* @__PURE__ */ jsx(
979
- PageSizeURLQuery,
980
- {
981
- options: ["8", "16", "32", "64"],
982
- defaultValue: response?.currentData?.meta?.pagination?.pageSize.toString()
983
- }
984
- ),
985
- /* @__PURE__ */ jsx(
986
- PaginationURLQuery,
987
- {
988
- pagination: {
989
- pageCount: response?.currentData?.meta?.pagination?.pageCount || 0
990
- }
991
- }
992
- )
993
- ] })
994
- ] }) }),
995
- releaseModalShown && /* @__PURE__ */ jsx(
996
- ReleaseModal,
997
- {
998
- handleClose: toggleAddReleaseModal,
999
- handleSubmit: handleAddRelease,
1000
- isLoading: isSubmittingForm,
1001
- initialValues: INITIAL_FORM_VALUES
1002
- }
1003
- )
1004
- ] });
1005
- };
1006
1314
  const App = () => {
1007
1315
  return /* @__PURE__ */ jsx(CheckPagePermissions, { permissions: PERMISSIONS.main, children: /* @__PURE__ */ jsxs(Switch, { children: [
1008
1316
  /* @__PURE__ */ jsx(Route, { exact: true, path: `/plugins/${pluginId}`, component: ReleasesPage }),
@@ -1012,4 +1320,4 @@ const App = () => {
1012
1320
  export {
1013
1321
  App
1014
1322
  };
1015
- //# sourceMappingURL=App-L1jSxCiL.mjs.map
1323
+ //# sourceMappingURL=App-bpzO2Ljh.mjs.map