Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/node_modules
/build
.idea/
.editorconfig
3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useAppDispatch } from "./store";
import { fetchOcVersion, fetchUserInfo } from "./slices/userInfoSlice";
import { subscribeToAuthEvents } from "./utils/broadcastSync";
import { useTableFilterStateValidation } from "./hooks/useTableFilterStateValidation";
import Playlists from "./components/events/Playlists";

function App() {
const dispatch = useAppDispatch();
Expand Down Expand Up @@ -47,6 +48,8 @@ function App() {

<Route path={"/events/series"} element={<Series />} />

<Route path={"/events/playlists"} element={<Playlists />} />

<Route path={"/recordings/recordings"} element={<Recordings />} />

<Route path={"/systems/jobs"} element={<Jobs />} />
Expand Down
43 changes: 43 additions & 0 deletions src/components/events/Playlists.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
;import TablePage from "../shared/TablePage";
import { fetchPlaylists } from "../../slices/playlistSlice";
import { loadPlaylistsIntoTable } from "../../thunks/tableThunks";
import { getTotalPlaylists } from "../../selectors/playlistSelectors";
import { eventsLinks } from "./partials/EventsNavigation";
import { playlistsTemplateMap } from "../../configs/tableConfigs/playlistsTableMap";
import PlaylistDetailsModal from "./partials/modals/PlaylistDetailsModal";
import { useAppDispatch } from "../../store";
import { fetchAclDefaults } from "../../slices/aclSlice";


/**
* This component renders the table view of playlists
*/
const Playlists = () => {
const dispatch = useAppDispatch();

const onNewPlaylistModal = async () => {
await dispatch(fetchAclDefaults());
};

return <>
<TablePage
resource={"playlists"}
fetchResource={fetchPlaylists}
loadResourceIntoTable={loadPlaylistsIntoTable}
getTotalResources={getTotalPlaylists}
navBarLinks={eventsLinks}
navBarCreate={{
accessRole: "ROLE_UI_PLAYLISTS_CREATE",
onShowModal: onNewPlaylistModal,
text: "EVENTS.EVENTS.ADD_PLAYLIST",
resource: "playlists",
}}
caption={"EVENTS.PLAYLISTS.TABLE.CAPTION"}
templateMap={playlistsTemplateMap}
/>

<PlaylistDetailsModal />
</>;
};

export default Playlists;
31 changes: 18 additions & 13 deletions src/components/events/partials/EventsNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@ import { ParseKeys } from "i18next";
* Utility file for the navigation bar
*/
export const eventsLinks: {
path: string,
accessRole: string,
text: ParseKeys
path: string,
accessRole: string,
text: ParseKeys
}[] = [
{
path: "/events/events",
accessRole: "ROLE_UI_EVENTS_VIEW",
text: "EVENTS.EVENTS.NAVIGATION.EVENTS",
},
{
path: "/events/series",
accessRole: "ROLE_UI_SERIES_VIEW",
text: "EVENTS.EVENTS.NAVIGATION.SERIES",
},
{
path: "/events/events",
accessRole: "ROLE_UI_EVENTS_VIEW",
text: "EVENTS.EVENTS.NAVIGATION.EVENTS",
},
{
path: "/events/series",
accessRole: "ROLE_UI_SERIES_VIEW",
text: "EVENTS.EVENTS.NAVIGATION.SERIES",
},
{
path: "/events/playlists",
accessRole: "ROLE_UI_EVENTS_VIEW", // TODO
text: "EVENTS.PLAYLISTS.TABLE.CAPTION",
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ import ModalContentTable from "../../../shared/modals/ModalContentTable";
import { addNotification } from "../../../../slices/notificationSlice";
import { NOTIFICATION_CONTEXT } from "../../../../configs/modalConfig";

type InitialValues = {
[key: string]: string | string[];

export type MetadataValues = {
[key: string]: string | string[];
}

/**
Expand All @@ -44,7 +45,7 @@ const DetailsMetadataTab = ({
catalog: MetadataCatalog;
}, any> // (id: string, values: { [key: string]: any }, catalog: MetadataCatalog) => void,
editAccessRole: string,
formikRef?: React.RefObject<FormikProps<InitialValues> | null>
formikRef?: React.RefObject<FormikProps<MetadataValues> | null>
header?: ParseKeys
}) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -99,7 +100,7 @@ const DetailsMetadataTab = ({
>
{metadata.map(catalog => (
// initialize form
<Formik<InitialValues>
<Formik<MetadataValues>
key={catalog.flavor}
enableReinitialize
initialValues={getInitialValues(catalog)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useCallback } from "react";
import { FormikProps } from "formik";

import { PlaylistEntry } from "../../../../slices/playlistDetailsSlice";
import WizardNavigationButtons from "../../../shared/wizard/WizardNavigationButtons";
import PlaylistEntriesEditor from "./PlaylistEntriesEditor";
import ModalContentTable from "../../../shared/modals/ModalContentTable";


interface RequiredFormProps {
entries: PlaylistEntry[],
}

/**
* Wizard page for adding entries to a new playlist.
* Stores entries in Formik values rather than Redux state.
*/
const NewPlaylistEntriesPage = <T extends RequiredFormProps>({
formik,
nextPage,
previousPage,
}: {
formik: FormikProps<T>,
nextPage: (values: T) => void,
previousPage: (values: T) => void,
}) => {
const entries = formik.values.entries;

const setEntries = useCallback((updated: PlaylistEntry[]) => {
void formik.setFieldValue("entries", updated);
}, [formik]);

return <>
<ModalContentTable>
<PlaylistEntriesEditor
entries={entries}
setEntries={setEntries}
/>
</ModalContentTable>

<WizardNavigationButtons
noValidation
previousPage={previousPage}
nextPage={nextPage}
formik={formik}
/>
</>;
};

export default NewPlaylistEntriesPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useEffect } from "react";
import { ParseKeys } from "i18next";

import ResourceDetailsAccessPolicyTab from "../../../shared/modals/ResourceDetailsAccessPolicyTab";
import {
getPlaylistDetailsAcl,
getPlaylistDetailsPolicyTemplateId,
} from "../../../../selectors/playlistDetailsSelectors";
import { fetchPlaylistDetails, updatePlaylistAccess } from "../../../../slices/playlistDetailsSlice";
import { removeNotificationWizardForm } from "../../../../slices/notificationSlice";
import { useAppDispatch, useAppSelector } from "../../../../store";


/**
* This component manages the access policy tab of the playlist details modal
*/
const PlaylistDetailsAccessTab = ({
playlistId,
header,
policyChanged,
setPolicyChanged,
}: {
playlistId: string,
header: ParseKeys,
policyChanged: boolean,
setPolicyChanged: (value: boolean) => void,
}) => {
const dispatch = useAppDispatch();

const policies = useAppSelector(state => getPlaylistDetailsAcl(state));
const policyTemplateId = useAppSelector(state => getPlaylistDetailsPolicyTemplateId(state));

useEffect(() => {
dispatch(removeNotificationWizardForm());
}, [dispatch]);

return <ResourceDetailsAccessPolicyTab
resourceId={playlistId}
header={header}
buttonText={"EVENTS.PLAYLISTS.DETAILS.ACCESS.ACCESS_POLICY.LABEL"}
descriptionText={"EVENTS.PLAYLISTS.DETAILS.ACCESS.ACCESS_POLICY.DESCRIPTION"}
policies={policies}
policyTemplateId={policyTemplateId}
fetchAccessPolicies={fetchPlaylistDetails}
saveNewAccessPolicies={updatePlaylistAccess}
policyTableHeaderText={"EVENTS.PLAYLISTS.DETAILS.ACCESS.NON_USER_ROLES"}
policyTableRoleText={"EVENTS.PLAYLISTS.DETAILS.ACCESS.ROLE"}
policyTableNewText={"EVENTS.PLAYLISTS.DETAILS.ACCESS.NEW"}
userPolicyTableHeaderText={"EVENTS.PLAYLISTS.DETAILS.ACCESS.USERS"}
userPolicyTableRoleText={"EVENTS.PLAYLISTS.DETAILS.ACCESS.USER"}
userPolicyTableNewText={"EVENTS.PLAYLISTS.DETAILS.ACCESS.NEW_USER"}
editAccessRole={"ROLE_UI_PLAYLISTS_DETAILS_ACL_EDIT"}
viewUsersAccessRole={"ROLE_UI_PLAYLISTS_DETAILS_ACL_USER_ROLES_VIEW"}
viewNonUsersAccessRole={"ROLE_UI_PLAYLISTS_DETAILS_ACL_NONUSER_ROLES_VIEW"}
policyChanged={policyChanged}
setPolicyChanged={setPolicyChanged}
/>;
};

export default PlaylistDetailsAccessTab;
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useCallback } from "react";

import { useAppDispatch, useAppSelector } from "../../../../store";
import {
getPlaylistDetailsEntries,
getPlaylistDetailsEntriesChanged,
} from "../../../../selectors/playlistDetailsSelectors";
import {
PlaylistEntry,
fetchPlaylistDetails,
setPlaylistDetailsEntries,
setPlaylistEntriesChanged,
updatePlaylistEntries,
} from "../../../../slices/playlistDetailsSlice";
import { SaveEditFooter } from "../../../shared/SaveEditFooter";
import PlaylistEntriesEditor from "./PlaylistEntriesEditor";
import ModalContentTable from "../../../shared/modals/ModalContentTable";


/* Entries management for existing playlist details */
const PlaylistDetailsEntriesTab = ({
playlistId,
}: {
playlistId: string,
}) => {
const dispatch = useAppDispatch();

const entries = useAppSelector(
state => getPlaylistDetailsEntries(state),
);

const entriesChanged = useAppSelector(
state => getPlaylistDetailsEntriesChanged(state),
);

const setEntries = useCallback((updated: PlaylistEntry[]) => {
dispatch(setPlaylistDetailsEntries(updated));
dispatch(setPlaylistEntriesChanged(true));
}, [dispatch]);

const saveEntries = () => {
void dispatch(updatePlaylistEntries({ id: playlistId, entries }));
};

const resetEntries = () => {
void dispatch(fetchPlaylistDetails(playlistId));
};

return <>
<ModalContentTable>
<PlaylistEntriesEditor
entries={entries}
setEntries={setEntries}
showEngageLinks
/>
</ModalContentTable>

<SaveEditFooter
active={entriesChanged}
isValid={true}
reset={resetEntries}
submit={saveEntries}
/>
</>;
};

export default PlaylistDetailsEntriesTab;
Loading
Loading