Skip to content

Commit

Permalink
Merge pull request #269 from Offline-Project/241-dca-5-create-the-con…
Browse files Browse the repository at this point in the history
…tent-upload-and-editing-interface

241 dca 5 create the content upload and editing interface
  • Loading branch information
sebpalluel authored Feb 5, 2024
2 parents c590314 + dfa4377 commit 9264ed0
Show file tree
Hide file tree
Showing 55 changed files with 1,590 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// ContentSpaceSheet,
// ContentSpaceSheetSkeleton,
// } from '@features/back-office/content-spaces';
import { ContentSpaceSheet } from '@features/back-office/content-spaces';
import { getContentSpaceWithPassesOrganizer } from '@features/back-office/content-spaces-api';
import { getCurrentUser } from '@next/next-auth/user';
import { SheetContent } from '@ui/components';
Expand All @@ -24,11 +25,10 @@ async function ContentSpaceSheetPageContent({
});
if (!contentSpace) return notFound();
return (
<div>ContentSpaceSheetPageContent</div>
// <ContentSpaceSheet
// contentSpace={contentSpace}
// organizerId={user.role.organizerId}
// />
<ContentSpaceSheet
contentSpace={contentSpace}
organizerId={user.role.organizerId}
/>
);
}

Expand Down
16 changes: 15 additions & 1 deletion libs/features/back-office/content-spaces-types/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import type { GetContentSpacesFromOrganizerIdTableQuery } from '@gql/admin/types';
import { FileSummary } from '@bytescale/sdk';
import type {
GetContentSpacesFromOrganizerIdTableQuery,
GetContentSpaceWithPassesOrganizerQuery,
} from '@gql/admin/types';

export type ContentSpaceFromOrganizerTable = NonNullable<
NonNullable<
GetContentSpacesFromOrganizerIdTableQuery['organizer']
>['contentSpaces']
>[number];

export type ContentSpaceFromOrganizerWithPasses = NonNullable<
GetContentSpaceWithPassesOrganizerQuery['contentSpace']
>;

export type EventPass = ContentSpaceFromOrganizerWithPasses['eventPasses'][0];

export interface ContentSpaceFileWithName extends FileSummary {
fileName: string;
}
3 changes: 2 additions & 1 deletion libs/features/back-office/content-spaces/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { ContentSpaceSheet } from './lib/pages/ContentSpaceSheet/ContentSpaceSheet';
export {
ContentSpaceTableSkeleton,
ContentSpacesPage,
} from './lib/pages/ContentSpacesPage';
} from './lib/pages/ContentSpacesPage/ContentSpacesPage';
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { FileWrapper } from '@file-upload/admin';
import { revalidateTag } from 'next/cache';
import {
deleteContentSpaceFile,
DeleteContentSpaceFileProps,
} from './deleteContentSpaceFile';

jest.mock('@file-upload/admin');
jest.mock('next/cache');

describe('deleteContentSpaceFile', () => {
const mockDeleteFile = jest.fn();
const mockRevalidateTag = jest.fn();
beforeAll(() => {
// Mock the FileWrapper instance method
FileWrapper.prototype.deleteFile = mockDeleteFile;

// Mock the revalidateTag function
(revalidateTag as jest.Mock) = mockRevalidateTag;
});
beforeEach(() => {
jest.clearAllMocks();
});
it('should call deleteFile and revalidateTag with correct arguments', async () => {
const props: DeleteContentSpaceFileProps = {
organizerId: 'testOrganizerId',
contentSpaceId: 'testContentSpaceId',
filePath:
'/local/organizers/testOrganizerId/content-spaces/testContentSpaceId/testFile',
};

await deleteContentSpaceFile(props);

expect(mockDeleteFile).toHaveBeenCalledWith({
accountId: expect.any(String), // UPLOAD_ACCOUNT_ID
filePath:
'/local/organizers/testOrganizerId/content-spaces/testContentSpaceId/testFile',
});

expect(mockRevalidateTag).toHaveBeenCalledWith(
`${props.organizerId}-${props.contentSpaceId}-getContentSpaceFiles`,
);
});

it('should handle error from deleteFile', async () => {
const props: DeleteContentSpaceFileProps = {
organizerId: 'testOrganizerId',
contentSpaceId: 'testContentSpaceId',
filePath:
'/local/organizers/testOrganizerId/content-spaces/testContentSpaceId/testFile',
};

mockDeleteFile.mockRejectedValueOnce(new Error('Test error'));

await expect(deleteContentSpaceFile(props)).rejects.toThrow('Test error');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use server';

import env from '@env/server';
import { GetContentSpaceFolderPath } from '@features/content-space-common';
import { FileWrapper } from '@file-upload/admin';
import { revalidateTag } from 'next/cache';

export type DeleteContentSpaceFileProps = GetContentSpaceFolderPath & {
filePath: string;
};

export async function deleteContentSpaceFile({
organizerId,
contentSpaceId,
filePath,
}: DeleteContentSpaceFileProps) {
const fileApi = new FileWrapper();
await fileApi.deleteFile({ accountId: env.UPLOAD_ACCOUNT_ID, filePath });
revalidateTag(`${organizerId}-${contentSpaceId}-getContentSpaceFiles`);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import env from '@env/server';
import { FileWrapper } from '@file-upload/admin';
import { revalidateTag } from 'next/cache';
import {
deleteContentSpaceFiles,
DeleteContentSpaceFilesProps,
} from './deleteContentSpaceFiles';

jest.mock('@file-upload/admin');
jest.mock('next/cache');

describe('deleteContentSpaceFiles', () => {
const mockDeleteFilesBatchWithRetry = jest.fn();
const mockRevalidateTag = jest.fn();
beforeAll(() => {
// Mock the FileWrapper instance method
FileWrapper.prototype.deleteFilesBatchWithRetry =
mockDeleteFilesBatchWithRetry;

// Mock the revalidateTag function
(revalidateTag as jest.Mock) = mockRevalidateTag;
});
beforeEach(() => {
jest.clearAllMocks();
});
it('should call deleteFilesBatchWithRetry and revalidateTag with correct arguments', async () => {
const props: DeleteContentSpaceFilesProps = {
organizerId: 'testOrganizerId',
contentSpaceId: 'testContentSpaceId',
filesSelected: { file1: true, file2: false },
};

await deleteContentSpaceFiles(props);

expect(mockDeleteFilesBatchWithRetry).toHaveBeenCalledWith(
expect.any(String), // UPLOAD_ACCOUNT_ID
[
`/${env.UPLOAD_PATH_PREFIX}/organizers/testOrganizerId/content-spaces/testContentSpaceId/file1`,
], // filesToDelete
);

expect(mockRevalidateTag).toHaveBeenCalledWith(
`${props.organizerId}-${props.contentSpaceId}-getContentSpaceFiles`,
);
});
it('should call deleteFilesBatchWithRetry with all files when all are selected', async () => {
const props: DeleteContentSpaceFilesProps = {
organizerId: 'testOrganizerId',
contentSpaceId: 'testContentSpaceId',
filesSelected: { file1: true, file2: true },
};

await deleteContentSpaceFiles(props);

expect(mockDeleteFilesBatchWithRetry).toHaveBeenCalledWith(
expect.any(String),
[
`/${env.UPLOAD_PATH_PREFIX}/organizers/testOrganizerId/content-spaces/testContentSpaceId/file1`,
`/${env.UPLOAD_PATH_PREFIX}/organizers/testOrganizerId/content-spaces/testContentSpaceId/file2`,
],
);
});
it('should throw an error when no files are selected', async () => {
const props: DeleteContentSpaceFilesProps = {
organizerId: 'testOrganizerId',
contentSpaceId: 'testContentSpaceId',
filesSelected: {},
};

await expect(deleteContentSpaceFiles(props)).rejects.toThrow(
'No files to delete selected',
);
expect(mockDeleteFilesBatchWithRetry).not.toHaveBeenCalled();
expect(mockRevalidateTag).not.toHaveBeenCalled();
});

it('should not call deleteFilesBatchWithRetry when filesSelected only contains false', async () => {
const props: DeleteContentSpaceFilesProps = {
organizerId: 'testOrganizerId',
contentSpaceId: 'testContentSpaceId',
filesSelected: { file1: false, file2: false },
};

await expect(deleteContentSpaceFiles(props)).rejects.toThrow(
'No files to delete selected',
);
expect(mockDeleteFilesBatchWithRetry).not.toHaveBeenCalled();
expect(mockRevalidateTag).not.toHaveBeenCalled();
});

it('should handle error from deleteFilesBatchWithRetry', async () => {
const props: DeleteContentSpaceFilesProps = {
organizerId: 'testOrganizerId',
contentSpaceId: 'testContentSpaceId',
filesSelected: { file1: true },
};

mockDeleteFilesBatchWithRetry.mockRejectedValueOnce(
new Error('Test error'),
);

await expect(deleteContentSpaceFiles(props)).rejects.toThrow('Test error');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use server';

import env from '@env/server';
import {
getContentSpaceFolderPath,
type GetContentSpaceFolderPath,
} from '@features/content-space-common';
import { FileWrapper } from '@file-upload/admin';
import { RowSelectionState } from '@tanstack/react-table';
import { revalidateTag } from 'next/cache';

export type DeleteContentSpaceFilesProps = GetContentSpaceFolderPath & {
filesSelected: RowSelectionState;
};

export async function deleteContentSpaceFiles({
organizerId,
contentSpaceId,
filesSelected,
}: DeleteContentSpaceFilesProps) {
const folderPath = getContentSpaceFolderPath({
organizerId,
contentSpaceId,
});
const fileApi = new FileWrapper();
const filesToDelete = Object.entries(filesSelected)
.filter(([_fileName, selected]) => selected)
.map(([fileName, _selected]) => `${folderPath}/${fileName}`);
if (!filesToDelete.length) throw new Error('No files to delete selected');
await fileApi.deleteFilesBatchWithRetry(env.UPLOAD_ACCOUNT_ID, filesToDelete);
revalidateTag(`${organizerId}-${contentSpaceId}-getContentSpaceFiles`);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { FileSummary } from '@bytescale/sdk';
import env from '@env/server';
import {
GetContentSpaceFolderPath,
getContentSpaceFolderPath,
} from '@features/content-space-common';
import { FolderWrapper } from '@file-upload/admin';
import { cacheWithDynamicKeys } from '@next/cache';

export type GetContentSpaceFilesProps = GetContentSpaceFolderPath;

export const getContentSpaceFiles = cacheWithDynamicKeys(
async (props: GetContentSpaceFilesProps) => {
const folder = new FolderWrapper();
const folderPath = getContentSpaceFolderPath(props);
const list = await folder.listFolder({
accountId: env.UPLOAD_ACCOUNT_ID,
folderPath,
});
return list.items.filter((item): item is FileSummary => 'filePath' in item);
},
(props: [GetContentSpaceFilesProps]) => [
`${props[0].organizerId}-${props[0].contentSpaceId}-getContentSpaceFiles`,
],
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ContentSpaceFromOrganizerWithPasses } from '@features/back-office/content-spaces-types';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
TableSkeleton,
} from '@ui/components';
import { useTranslations } from 'next-intl';

export interface ContentSpaceEventPassesTableProps
extends Pick<ContentSpaceFromOrganizerWithPasses, 'eventPasses'> {}

export function ContentSpaceEventPassesTable({
eventPasses,
}: ContentSpaceEventPassesTableProps) {
const eventPassesByEvent = eventPasses.reduce(
(acc, eventPass) => {
const event = eventPass.event;
if (!event) return acc;
if (!acc[event.slug]) acc[event.slug] = [];
acc[event.slug].push(eventPass);
return acc;
},
{} as Record<string, ContentSpaceFromOrganizerWithPasses['eventPasses']>,
);

const t = useTranslations(
'OrganizerContentSpaces.Sheet.ContentSpaceEventPassesTable',
);

return (
<>
{Object.entries(eventPassesByEvent).map(([slug, eventPasses]) => (
<Table key={slug}>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">
{t('event-pass-for-event', { slug })}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{eventPasses.map((eventPass) => (
<TableRow key={eventPass.id}>
<TableCell>{eventPass.name}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
))}
</>
);
}

export function ContentSpaceEventPassesTableSkeleton() {
return <TableSkeleton rows={4} cols={2} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { EventPass } from '@features/back-office/content-spaces-types';

export const eventPassWorldCupFinale2023VIP: EventPass = {
id: '1',
name: 'VIP Pass',
event: {
slug: 'world-cup-finale-2023',
title: 'World Cup Finale 2023',
},
};

export const eventPassWorldCupFinale2023Premium: EventPass = {
id: '2',
name: 'Premium Pass',
event: {
slug: 'world-cup-finale-2023',
title: 'World Cup Finale 2023',
},
};

export const eventPassWorldCupFinale2023MeetAndGreet: EventPass = {
id: '3',
name: 'Meet and Greet',
event: {
slug: 'world-cup-finale-2023',
title: 'World Cup Finale 2023',
},
};

export const eventPassesWorldCupFinale: EventPass[] = [
eventPassWorldCupFinale2023VIP,
eventPassWorldCupFinale2023Premium,
eventPassWorldCupFinale2023MeetAndGreet,
];
Loading

0 comments on commit 9264ed0

Please sign in to comment.