diff --git a/src/app/(private)/dashboard/about-us/(about-us)/edit/page.tsx b/src/app/(private)/dashboard/about-us/(about-us)/edit/page.tsx index 91e514a4..8a277699 100644 --- a/src/app/(private)/dashboard/about-us/(about-us)/edit/page.tsx +++ b/src/app/(private)/dashboard/about-us/(about-us)/edit/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function AboutUsEditPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/about-us/contributors/edit/[id]/page.tsx b/src/app/(private)/dashboard/about-us/contributors/edit/[id]/page.tsx index 6bcc63d1..1f8d7c5a 100644 --- a/src/app/(private)/dashboard/about-us/contributors/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/about-us/contributors/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditContributorPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/about-us/links/edit/[id]/page.tsx b/src/app/(private)/dashboard/about-us/links/edit/[id]/page.tsx index 96914adf..a8e3dc83 100644 --- a/src/app/(private)/dashboard/about-us/links/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/about-us/links/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditAboutUsLinkPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/about-us/roles/edit/[id]/page.tsx b/src/app/(private)/dashboard/about-us/roles/edit/[id]/page.tsx index 1df00a89..37383571 100644 --- a/src/app/(private)/dashboard/about-us/roles/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/about-us/roles/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditRolePage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/about-us/versions/edit/[id]/page.tsx b/src/app/(private)/dashboard/about-us/versions/edit/[id]/page.tsx index 4ae0c5e5..dcb1ad76 100644 --- a/src/app/(private)/dashboard/about-us/versions/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/about-us/versions/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditMilestonePage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/academic-semesters/edit/[id]/page.tsx b/src/app/(private)/dashboard/academic-semesters/edit/[id]/page.tsx index 85847ad4..a87d0d62 100644 --- a/src/app/(private)/dashboard/academic-semesters/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/academic-semesters/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditAcademicSemesterPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/banners/edit/[id]/page.tsx b/src/app/(private)/dashboard/banners/edit/[id]/page.tsx index 2d91d6b9..a8cd151c 100644 --- a/src/app/(private)/dashboard/banners/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/banners/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditBannerPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/calendar-events/edit/[id]/page.tsx b/src/app/(private)/dashboard/calendar-events/edit/[id]/page.tsx index 97958c08..43a64d08 100644 --- a/src/app/(private)/dashboard/calendar-events/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/calendar-events/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditCalendarEventPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/day-swaps/edit/[id]/page.tsx b/src/app/(private)/dashboard/day-swaps/edit/[id]/page.tsx index e0b574af..d10598c9 100644 --- a/src/app/(private)/dashboard/day-swaps/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/day-swaps/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditDaySwapPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/departments/edit/[id]/page.tsx b/src/app/(private)/dashboard/departments/edit/[id]/page.tsx index 0b4f581a..28a4c38e 100644 --- a/src/app/(private)/dashboard/departments/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/departments/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditDepartmentPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/guide-articles/edit/[id]/page.tsx b/src/app/(private)/dashboard/guide-articles/edit/[id]/page.tsx index 1086716b..4b824bac 100644 --- a/src/app/(private)/dashboard/guide-articles/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/guide-articles/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditGuideArticlePage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/holidays/edit/[id]/page.tsx b/src/app/(private)/dashboard/holidays/edit/[id]/page.tsx index 9efc47f0..dd86dd12 100644 --- a/src/app/(private)/dashboard/holidays/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/holidays/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditHolidayPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/map/aeds/edit/[id]/page.tsx b/src/app/(private)/dashboard/map/aeds/edit/[id]/page.tsx index 8bd21454..616c63b9 100644 --- a/src/app/(private)/dashboard/map/aeds/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/map/aeds/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditAedPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/map/bicycle-showers/edit/[id]/page.tsx b/src/app/(private)/dashboard/map/bicycle-showers/edit/[id]/page.tsx index c36f240f..36fa604f 100644 --- a/src/app/(private)/dashboard/map/bicycle-showers/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/map/bicycle-showers/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditBicycleShowerPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/map/buildings/edit/[id]/page.tsx b/src/app/(private)/dashboard/map/buildings/edit/[id]/page.tsx index cada8e86..702208ad 100644 --- a/src/app/(private)/dashboard/map/buildings/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/map/buildings/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditBuildingPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/map/campuses/edit/[id]/page.tsx b/src/app/(private)/dashboard/map/campuses/edit/[id]/page.tsx index 576b7ce5..7a256845 100644 --- a/src/app/(private)/dashboard/map/campuses/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/map/campuses/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditCampusPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/map/food-spots/edit/[id]/page.tsx b/src/app/(private)/dashboard/map/food-spots/edit/[id]/page.tsx index 2e5e21f2..d4941c2c 100644 --- a/src/app/(private)/dashboard/map/food-spots/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/map/food-spots/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditFoodSpotPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/map/libraries/edit/[id]/page.tsx b/src/app/(private)/dashboard/map/libraries/edit/[id]/page.tsx index 8868aeab..9446bf47 100644 --- a/src/app/(private)/dashboard/map/libraries/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/map/libraries/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditLibraryPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/map/pink-boxes/edit/[id]/page.tsx b/src/app/(private)/dashboard/map/pink-boxes/edit/[id]/page.tsx index 244b4c01..87c507b1 100644 --- a/src/app/(private)/dashboard/map/pink-boxes/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/map/pink-boxes/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditPinkBoxPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/map/polinka-stations/edit/[id]/page.tsx b/src/app/(private)/dashboard/map/polinka-stations/edit/[id]/page.tsx index 2aa4108d..513b83b3 100644 --- a/src/app/(private)/dashboard/map/polinka-stations/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/map/polinka-stations/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditPolinkaStationPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/misc/(misc)/edit/page.tsx b/src/app/(private)/dashboard/misc/(misc)/edit/page.tsx index 9e4bf320..56aa38ce 100644 --- a/src/app/(private)/dashboard/misc/(misc)/edit/page.tsx +++ b/src/app/(private)/dashboard/misc/(misc)/edit/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function MobileConfigEditPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/misc/sks-opening-hours/edit/[id]/page.tsx b/src/app/(private)/dashboard/misc/sks-opening-hours/edit/[id]/page.tsx index 99627459..7edb606e 100644 --- a/src/app/(private)/dashboard/misc/sks-opening-hours/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/misc/sks-opening-hours/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditSksOpeningHoursPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/dashboard/notifications/topics/edit/[id]/page.tsx b/src/app/(private)/dashboard/notifications/topics/edit/[id]/page.tsx index 813d4123..90976825 100644 --- a/src/app/(private)/dashboard/notifications/topics/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/notifications/topics/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditNotificationTopicPage( diff --git a/src/app/(private)/dashboard/page.tsx b/src/app/(private)/dashboard/page.tsx index 1aa82bf3..731ffd49 100644 --- a/src/app/(private)/dashboard/page.tsx +++ b/src/app/(private)/dashboard/page.tsx @@ -1,7 +1,11 @@ import { RefreshCcw } from "lucide-react"; import { DashboardButton } from "@/components/presentation/dashboard-button"; -import { getUserDisplayName } from "@/features/authentication"; +import { + getUserDisplayName, + isAdmin, + isSolvroAdmin, +} from "@/features/authentication"; import { getAuthStateServer } from "@/features/authentication/server"; import { Resource } from "@/features/resources"; @@ -20,12 +24,21 @@ export default async function AdminPage() {
- + {isSolvroAdmin(user) || isAdmin(user) ? ( + + ) : ( + + )}
diff --git a/src/app/(private)/dashboard/student-organizations/edit/[id]/page.tsx b/src/app/(private)/dashboard/student-organizations/edit/[id]/page.tsx index 36210da7..538883c4 100644 --- a/src/app/(private)/dashboard/student-organizations/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/student-organizations/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditStudentOrganizationPage( diff --git a/src/app/(private)/dashboard/versions/edit/[id]/page.tsx b/src/app/(private)/dashboard/versions/edit/[id]/page.tsx index 94de20cf..29c8df71 100644 --- a/src/app/(private)/dashboard/versions/edit/[id]/page.tsx +++ b/src/app/(private)/dashboard/versions/edit/[id]/page.tsx @@ -1,4 +1,5 @@ -import { AbstractResourceEditPage, Resource } from "@/features/resources"; +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; import type { ResourceEditPageProps } from "@/types/components"; export default function EditVersionPage(props: ResourceEditPageProps) { diff --git a/src/app/(private)/drafts/(resources)/guide-article-drafts/edit/[id]/page.tsx b/src/app/(private)/drafts/(resources)/guide-article-drafts/edit/[id]/page.tsx new file mode 100644 index 00000000..1c6c6cdc --- /dev/null +++ b/src/app/(private)/drafts/(resources)/guide-article-drafts/edit/[id]/page.tsx @@ -0,0 +1,13 @@ +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; +import type { ResourceEditPageProps } from "@/types/components"; + +export default function EditGuideArticlePage(props: ResourceEditPageProps) { + return ( + + ); +} diff --git a/src/app/(private)/drafts/(resources)/student-organization-drafts/edit/[id]/page.tsx b/src/app/(private)/drafts/(resources)/student-organization-drafts/edit/[id]/page.tsx new file mode 100644 index 00000000..44aef666 --- /dev/null +++ b/src/app/(private)/drafts/(resources)/student-organization-drafts/edit/[id]/page.tsx @@ -0,0 +1,15 @@ +import { Resource } from "@/features/resources"; +import { AbstractResourceEditPage } from "@/features/resources/server"; +import type { ResourceEditPageProps } from "@/types/components"; + +export default function EditStudentOrganizationPage( + props: ResourceEditPageProps, +) { + return ( + + ); +} diff --git a/src/app/(private)/drafts/layout.tsx b/src/app/(private)/drafts/layout.tsx new file mode 100644 index 00000000..3fda4972 --- /dev/null +++ b/src/app/(private)/drafts/layout.tsx @@ -0,0 +1,10 @@ +import { AbstractResourceLayout } from "@/features/resources/server"; +import type { WrapperProps } from "@/types/components"; + +export default function ReviewLayout({ children }: WrapperProps) { + return ( + + {children} + + ); +} diff --git a/src/app/(private)/drafts/page.tsx b/src/app/(private)/drafts/page.tsx new file mode 100644 index 00000000..653b48e1 --- /dev/null +++ b/src/app/(private)/drafts/page.tsx @@ -0,0 +1,5 @@ +import { DraftList } from "@/features/review/server"; + +export default function ReviewPage() { + return ; +} diff --git a/src/app/(private)/review/page.tsx b/src/app/(private)/review/page.tsx index 9e99cf89..653b48e1 100644 --- a/src/app/(private)/review/page.tsx +++ b/src/app/(private)/review/page.tsx @@ -1,6 +1,5 @@ -import { ErrorMessage } from "@/components/presentation/error-message"; -import { ApplicationError } from "@/config/enums"; +import { DraftList } from "@/features/review/server"; export default function ReviewPage() { - return ; + return ; } diff --git a/src/features/abstract-resource-form/components/abstract-resource-form.tsx b/src/features/abstract-resource-form/components/abstract-resource-form.tsx index 89dfc2bf..2fe6353b 100644 --- a/src/features/abstract-resource-form/components/abstract-resource-form.tsx +++ b/src/features/abstract-resource-form/components/abstract-resource-form.tsx @@ -1,9 +1,17 @@ import { get } from "react-hook-form"; import { ApiImage } from "@/features/backend/server"; -import { getResourceMetadata } from "@/features/resources"; +import { + RelationType, + getResourceMetadata, + getResourceQueryName, + getResourceRelationDefinitions, +} from "@/features/resources"; import type { Resource } from "@/features/resources"; -import type { ResourceDefaultValues } from "@/features/resources/types"; +import type { + ResourceDefaultValues, + XToManyResource, +} from "@/features/resources/types"; import type { ExistingImages, ResourceCreatePageProps, @@ -60,6 +68,17 @@ export async function AbstractResourceForm({ (defaultValues as Record)[key] = parsed; } + const relationDefinitions = getResourceRelationDefinitions(resource); + for (const [relation, definition] of typedEntries(relationDefinitions)) { + if (definition.type === RelationType.ManyToOne) { + continue; + } + const queryName = getResourceQueryName(relation as XToManyResource); + if (!(queryName in (defaultValues as Record))) { + (defaultValues as Record)[queryName] = []; + } + } + const relatedResources = await fetchRelatedResources(resource); const pivotResources = await fetchPivotResources( metadata.form.inputs.relationInputs, diff --git a/src/features/abstract-resource-form/components/arf-controller.tsx b/src/features/abstract-resource-form/components/arf-controller.tsx index b80a4b06..82c2ec1c 100644 --- a/src/features/abstract-resource-form/components/arf-controller.tsx +++ b/src/features/abstract-resource-form/components/arf-controller.tsx @@ -8,9 +8,10 @@ import { toast } from "sonner"; import { ReturnButton } from "@/components/presentation/return-button"; import { Form } from "@/components/ui/form"; +import { isSolvroAdmin, useAuthentication } from "@/features/authentication"; import { fetchMutation, useMutationWrapper } from "@/features/backend"; import type { ModifyResourceResponse } from "@/features/backend/types"; -import { declineNoun } from "@/features/polish"; +import { GrammaticalCase, declineNoun } from "@/features/polish"; import type { Resource } from "@/features/resources"; import { DeleteButtonWithDialog, @@ -28,6 +29,7 @@ import type { ResourcePk, RoutableResource, } from "@/features/resources/types"; +import { ApproveButton } from "@/features/review"; import { useRouter } from "@/hooks/use-router"; import { getToastMessages } from "@/lib/get-toast-messages"; import { cn } from "@/lib/utils"; @@ -58,15 +60,18 @@ export function ArfController({ relatedResources, pivotResources, className, + draft = false, }: ResourceFormProps & { defaultValues: ResourceDefaultValues; existingImages: ExistingImages; relatedResources: ResourceRelations; pivotResources: ResourcePivotRelationData; + draft?: boolean; }) { const schema = RESOURCE_SCHEMAS[resource]; const router = useRouter(); const relationContext = useArfRelation(); + const { user } = useAuthentication(); const form = useForm>({ // Maybe try extracting the id from the defaultValues and passing it as an editedResourceId prop resolver: zodResolver(schema) as Resolver>, @@ -112,6 +117,7 @@ export function ArfController({ const response = await fetchMutation>(endpoint, { body, resource, + draft: draft || !isSolvroAdmin(user), ...mutationOptions, }); const wasCreated = mutationOptions.method === "POST"; @@ -195,8 +201,18 @@ export function ArfController({ onSubmit={onSubmit} confirmationMessage={confirmationMessage} > - {submitLabel} {declensions.accusative} + {draft + ? `Zapisz ${declineNoun("draft", { case: GrammaticalCase.Accusative })}` + : `${submitLabel} ${declensions.accusative}`} + + {draft && isSolvroAdmin(user) ? ( + + ) : null} {isEditing && metadata.deletable !== false ? ( { resource: T; relatedResources: ResourceRelations; dragHandleProps?: Omit; + actions?: ReactNode; } /** TODO: pass custom delete functionality as a prop, which would eliminate this helper */ @@ -38,7 +39,8 @@ const isStudentOrganizationProps = ( } => props.resource === Resource.StudentOrganizations; export function ArlItem(props: ItemProps) { - const { ref, item, resource, relatedResources, dragHandleProps } = props; + const { ref, item, resource, relatedResources, dragHandleProps, actions } = + props; const metadata = getResourceMetadata(resource); const id = getResourcePkValue(resource, item); @@ -100,14 +102,18 @@ export function ArlItem(props: ItemProps) { )}
diff --git a/src/features/abstract-resource-list/components/toggle-status-button.tsx b/src/features/abstract-resource-list/components/toggle-status-button.tsx index c5e41209..1d8be155 100644 --- a/src/features/abstract-resource-list/components/toggle-status-button.tsx +++ b/src/features/abstract-resource-list/components/toggle-status-button.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useQueryClient } from "@tanstack/react-query"; import { Archive, ArchiveRestore } from "lucide-react"; import { toast } from "sonner"; diff --git a/src/features/abstract-resource-list/index.ts b/src/features/abstract-resource-list/index.ts index 928cd835..eccf305f 100644 --- a/src/features/abstract-resource-list/index.ts +++ b/src/features/abstract-resource-list/index.ts @@ -1,3 +1,6 @@ export * from "./components/abstract-resource-list"; +export * from "./components/arl-item"; + export * from "./hooks/use-order-mutation"; + export * from "./utils/calculate-new-sort-value"; diff --git a/src/features/authentication/data/permissions.ts b/src/features/authentication/data/permissions.ts new file mode 100644 index 00000000..aaa8af9f --- /dev/null +++ b/src/features/authentication/data/permissions.ts @@ -0,0 +1,3 @@ +export const SOLVRO_ADMINS_ONLY = ["solvro_admin"] as const; +export const ADMINS_ONLY = ["admin", ...SOLVRO_ADMINS_ONLY] as const; +export const ANY_AUTHENTICATED_ROLE = ["user", ...ADMINS_ONLY] as const; diff --git a/src/features/authentication/data/route-permissions.ts b/src/features/authentication/data/route-permissions.ts index 129e0701..fba7fbdb 100644 --- a/src/features/authentication/data/route-permissions.ts +++ b/src/features/authentication/data/route-permissions.ts @@ -4,40 +4,39 @@ import { Resource } from "@/features/resources"; import type { RoutableResource } from "@/features/resources/types"; import type { RecordIntersection } from "@/types/helpers"; -const SOLVRO_ADMINS_ONLY = ["solvro_admin"] as const; -const ADMINS_ONLY = ["admin", ...SOLVRO_ADMINS_ONLY] as const; -const ANY_AUTHENTICATED_ROLE = ["user", ...ADMINS_ONLY] as const; +import { ANY_AUTHENTICATED_ROLE, SOLVRO_ADMINS_ONLY } from "./permissions"; export const ROUTE_PERMISSIONS = { [`/${Resource.Dashboard}`]: ANY_AUTHENTICATED_ROLE, + "/drafts": ANY_AUTHENTICATED_ROLE, "/review": SOLVRO_ADMINS_ONLY, - [`/${Resource.AboutUs}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.AboutUsLinks}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.AcademicSemesters}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Aeds}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Banners}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.BicycleShowers}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Buildings}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.CalendarEvents}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Campuses}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Contributors}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.DaySwaps}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Departments}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.FoodSpots}`]: ANY_AUTHENTICATED_ROLE, + [`/${Resource.AboutUs}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.AboutUsLinks}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.AcademicSemesters}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.Aeds}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.Banners}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.BicycleShowers}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.Buildings}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.CalendarEvents}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.Campuses}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.Contributors}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.DaySwaps}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.Departments}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.FoodSpots}`]: SOLVRO_ADMINS_ONLY, [`/${Resource.GuideArticles}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Holidays}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Libraries}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Map}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Milestones}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.MobileConfig}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Notifications}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.NotificationTopics}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.PinkBoxes}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.PolinkaStations}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Roles}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.SksOpeningHours}`]: ANY_AUTHENTICATED_ROLE, + [`/${Resource.Holidays}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.Libraries}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.Map}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.Milestones}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.MobileConfig}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.Notifications}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.NotificationTopics}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.PinkBoxes}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.PolinkaStations}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.Roles}`]: SOLVRO_ADMINS_ONLY, + [`/${Resource.SksOpeningHours}`]: SOLVRO_ADMINS_ONLY, [`/${Resource.StudentOrganizations}`]: ANY_AUTHENTICATED_ROLE, - [`/${Resource.Versions}`]: ANY_AUTHENTICATED_ROLE, + [`/${Resource.Versions}`]: SOLVRO_ADMINS_ONLY, } satisfies RecordIntersection< `/${RoutableResource}`, Route, diff --git a/src/features/authentication/index.ts b/src/features/authentication/index.ts index 50ba3d97..6f3c1ca7 100644 --- a/src/features/authentication/index.ts +++ b/src/features/authentication/index.ts @@ -6,4 +6,6 @@ export * from "./constants"; export * from "./hooks/use-authentication"; export * from "./utils/get-user-display-name"; +export * from "./utils/is-admin"; +export * from "./utils/is-solvro-admin"; export * from "./utils/parse-auth-cookie"; diff --git a/src/features/authentication/utils/is-admin.ts b/src/features/authentication/utils/is-admin.ts new file mode 100644 index 00000000..f11ba372 --- /dev/null +++ b/src/features/authentication/utils/is-admin.ts @@ -0,0 +1,8 @@ +import { ADMINS_ONLY } from "../data/permissions"; +import type { User } from "../types/internal"; +import { getUserPermissions } from "./get-user-permissions"; + +export const isAdmin = (user: User | null): boolean => { + const permissions = getUserPermissions(user); + return ADMINS_ONLY.some((role) => permissions.includes(role)); +}; diff --git a/src/features/authentication/utils/is-solvro-admin.ts b/src/features/authentication/utils/is-solvro-admin.ts new file mode 100644 index 00000000..1062f3a5 --- /dev/null +++ b/src/features/authentication/utils/is-solvro-admin.ts @@ -0,0 +1,8 @@ +import { SOLVRO_ADMINS_ONLY } from "../data/permissions"; +import type { User } from "../types"; +import { getUserPermissions } from "./get-user-permissions"; + +export const isSolvroAdmin = (user: User | null): boolean => { + const permissions = getUserPermissions(user); + return SOLVRO_ADMINS_ONLY.every((role) => permissions.includes(role)); +}; diff --git a/src/features/backend/data/api-error-messages.ts b/src/features/backend/data/api-error-messages.ts index d926eb87..b253df65 100644 --- a/src/features/backend/data/api-error-messages.ts +++ b/src/features/backend/data/api-error-messages.ts @@ -4,4 +4,5 @@ export const API_ERROR_MESSAGES: Record = { E_UNEXPECTED_ERROR: "Nastąpił nieoczekiwany błąd", E_VALIDATION_ERROR: "Wpisane dane są niekompletne lub nieprawidłowe", E_NOT_FOUND: "Nie znaleziono podanego zasobu", + E_UNAUTHORIZED_ACCESS: "Brak uprawnień do wyświetlenia tego zasobu", }; diff --git a/src/features/backend/lib/create-request.ts b/src/features/backend/lib/create-request.ts index 6eddd1b2..69023aa6 100644 --- a/src/features/backend/lib/create-request.ts +++ b/src/features/backend/lib/create-request.ts @@ -13,6 +13,7 @@ export function createRequest( { accessTokenOverride, resource, + draft = false, body, includeRelations = false, ...options @@ -29,7 +30,7 @@ export function createRequest( }; } - const endpointPrefix = getResourceEndpointPrefix(resource); + const endpointPrefix = getResourceEndpointPrefix(resource, draft); let queryParameters = getRelationQueryParameters(resource, includeRelations); if (queryParameters !== "") { queryParameters = `${endpoint.includes("?") ? "&" : "?"}${queryParameters}`; diff --git a/src/features/backend/types/internal.ts b/src/features/backend/types/internal.ts index a7ac9c03..3845c046 100644 --- a/src/features/backend/types/internal.ts +++ b/src/features/backend/types/internal.ts @@ -12,6 +12,7 @@ interface BaseRequestOptions extends Omit< headers?: Record; accessTokenOverride?: string; resource?: T; + draft?: boolean; } export interface QueryRequestOptions< T extends Resource, diff --git a/src/features/backend/utils/get-resource-endpoint-prefix.ts b/src/features/backend/utils/get-resource-endpoint-prefix.ts index 03d49eb0..2ab5bc21 100644 --- a/src/features/backend/utils/get-resource-endpoint-prefix.ts +++ b/src/features/backend/utils/get-resource-endpoint-prefix.ts @@ -1,5 +1,14 @@ import type { Resource } from "@/features/resources"; import { getResourceMetadata } from "@/features/resources/node"; -export const getResourceEndpointPrefix = (resource: Resource | undefined) => - resource == null ? "" : `${getResourceMetadata(resource).apiPath}/`; +export const getResourceEndpointPrefix = ( + resource: Resource | undefined, + draft: boolean, +) => { + if (resource == null) { + return ""; + } + const metadata = getResourceMetadata(resource); + const draftPath = draft ? metadata.apiDraftPath : null; + return `${draftPath ?? metadata.apiPath}/`; +}; diff --git a/src/features/polish/data/simple-noun-declensions.ts b/src/features/polish/data/simple-noun-declensions.ts index d5c05be1..321b6cb9 100644 --- a/src/features/polish/data/simple-noun-declensions.ts +++ b/src/features/polish/data/simple-noun-declensions.ts @@ -701,6 +701,27 @@ export const SIMPLE_NOUN_DECLENSIONS = { }, }, [Resource.VersionScreenshots]: REUSABLE_DECLENSIONS.screenshot, + draft: { + gender: GrammaticalGender.Masculine, + singular: { + nominative: "draft", + genitive: "draftu", + dative: "draftowi", + accusative: "draft", + instrumental: "draftem", + locative: "drafcie", + vocative: "drafcie", + }, + plural: { + nominative: "drafty", + genitive: "draftów", + dative: "draftom", + accusative: "drafty", + instrumental: "draftami", + locative: "draftach", + vocative: "drafty", + }, + }, image: { gender: GrammaticalGender.Neuter, singular: { diff --git a/src/features/resources/components/abstract-resource-edit-page-internal.tsx b/src/features/resources/components/abstract-resource-edit-page-internal.tsx index c62be9e6..3be85398 100644 --- a/src/features/resources/components/abstract-resource-edit-page-internal.tsx +++ b/src/features/resources/components/abstract-resource-edit-page-internal.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import { ErrorMessage } from "@/components/presentation/error-message"; import { ApplicationError } from "@/config/enums"; import { AbstractResourceForm } from "@/features/abstract-resource-form"; +import { getAuthStateServer } from "@/features/authentication/server"; import { fetchQuery } from "@/features/backend"; import type { GetResourceWithRelationsResponse } from "@/features/backend/types"; @@ -11,17 +12,25 @@ import type { RoutableResource } from "../types"; export async function AbstractResourceEditPageInternal({ resource, path, + draft = false, errorMessage, }: { resource: RoutableResource; path: string; + draft?: boolean; errorMessage: ReactNode; }) { + const authState = await getAuthStateServer(); let resourceData; try { const response = await fetchQuery< GetResourceWithRelationsResponse - >(path, { resource, includeRelations: true }); + >(path, { + resource, + includeRelations: true, + draft, + accessTokenOverride: authState?.accessToken, + }); resourceData = response.data; } catch { return ( @@ -34,6 +43,10 @@ export async function AbstractResourceEditPageInternal({ } return ( - + ); } diff --git a/src/features/resources/components/abstract-resource-edit-page.tsx b/src/features/resources/components/abstract-resource-edit-page.tsx index ff1ca755..9f1a97a1 100644 --- a/src/features/resources/components/abstract-resource-edit-page.tsx +++ b/src/features/resources/components/abstract-resource-edit-page.tsx @@ -13,8 +13,10 @@ import { AbstractResourceEditPageInternal } from "./abstract-resource-edit-page- export async function AbstractResourceEditPage({ resource, params, + draft = false, }: ResourceEditPageProps & { resource: RoutableResource; + draft?: boolean; }) { const metadata = getResourceMetadata(resource); const declensions = declineNoun(resource); @@ -24,6 +26,7 @@ export async function AbstractResourceEditPage({ Nie udało się wczytać {declensions.genitive}.} /> ); @@ -48,6 +51,7 @@ export async function AbstractResourceEditPage({ Nie istnieje{" "} diff --git a/src/features/resources/data/resource-metadata.ts b/src/features/resources/data/resource-metadata.ts index 8917adc4..26325d4b 100644 --- a/src/features/resources/data/resource-metadata.ts +++ b/src/features/resources/data/resource-metadata.ts @@ -569,6 +569,7 @@ export const RESOURCE_METADATA = { }, [Resource.GuideArticles]: { apiPath: "guide_articles", + apiDraftPath: "guide_article_drafts", orderable: true, itemMapper: (item) => ({ name: item.title, @@ -885,6 +886,7 @@ export const RESOURCE_METADATA = { }, [Resource.StudentOrganizations]: { apiPath: "student_organizations", + apiDraftPath: "student_organization_drafts", itemMapper: (item) => ({ name: item.name, description: item.shortDescription, diff --git a/src/features/resources/index.ts b/src/features/resources/index.ts index 901c2d25..cb5c966a 100644 --- a/src/features/resources/index.ts +++ b/src/features/resources/index.ts @@ -1,4 +1,3 @@ -export * from "./components/abstract-resource-edit-page"; export * from "./components/abstract-resource-group"; export * from "./components/delete-button-with-dialog"; export * from "./components/create-button"; diff --git a/src/features/resources/server.ts b/src/features/resources/server.ts index 98147b56..4504df95 100644 --- a/src/features/resources/server.ts +++ b/src/features/resources/server.ts @@ -1 +1,2 @@ +export * from "./components/abstract-resource-edit-page"; export * from "./components/abstract-resource-layout"; diff --git a/src/features/resources/types/index.ts b/src/features/resources/types/index.ts index 4ae57a39..e332aa7e 100644 --- a/src/features/resources/types/index.ts +++ b/src/features/resources/types/index.ts @@ -12,6 +12,7 @@ export type { ResourceSchemaKey, RoutableResource, SubmitFormConfirmationMessage, + SpecificResourceMetadata, } from "./internal"; export type { diff --git a/src/features/resources/types/internal.ts b/src/features/resources/types/internal.ts index 51156159..24566f4a 100644 --- a/src/features/resources/types/internal.ts +++ b/src/features/resources/types/internal.ts @@ -96,6 +96,8 @@ export type ResourceMetadata = Readonly<{ pk?: ResourceSchemaKey; /** A mapping of the client-side resources to their paths in the backend API. */ apiPath: string; + /** If the API supports draft models for this resource, the path to be used when creating drafts. */ + apiDraftPath?: string; /** The API version to be used when fetching this resource. Defaults to 1. */ apiVersion?: number; /** diff --git a/src/features/review/api/fetch-drafts.ts b/src/features/review/api/fetch-drafts.ts new file mode 100644 index 00000000..3e635856 --- /dev/null +++ b/src/features/review/api/fetch-drafts.ts @@ -0,0 +1,91 @@ +import "server-only"; + +import { isAdmin, isSolvroAdmin } from "@/features/authentication"; +import { getAuthStateServer } from "@/features/authentication/server"; +import type { User } from "@/features/authentication/types"; +import { fetchQuery } from "@/features/backend"; +import { getResourceMetadata } from "@/features/resources"; +import type { ResourceDataType } from "@/features/resources/types"; +import { typedEntries } from "@/utils"; + +import { DRAFT_TYPE_RESOURCES } from "../data/draft-type-resources"; +import type { DraftableResource, ResourceDraft } from "../types/internal"; + +async function fetchDraftsForAdmins(accessToken: string) { + try { + const { data } = await fetchQuery<{ data: ResourceDraft[] }>("drafts", { + accessTokenOverride: accessToken, + }); + return data; + } catch { + return []; + } +} + +/** Fetches drafts of the given user based on instance-level read permissions. */ +async function fetchUserOwnedDrafts(user: User, accessToken: string) { + const draftResourceEntries = typedEntries(DRAFT_TYPE_RESOURCES); + + const draftPathToInfo = draftResourceEntries + .map(([draftType, resource]) => ({ + draftType, + resource, + path: getResourceMetadata(resource).apiDraftPath, + })) + .filter((info) => info.path != null); + + const relevantPermissions = user.permissions.filter( + (permission) => + permission.action === "read" && + permission.instanceId != null && + draftPathToInfo.some((info) => info.path === permission.modelName), + ); + if (relevantPermissions.length === 0) { + return []; + } + + const draftPromises = relevantPermissions.map(async (permission) => { + const info = draftPathToInfo.find( + (index) => index.path === permission.modelName, + ); + if (info == null) { + return null; + } + const { resource, draftType } = info; + + try { + const response = await fetchQuery<{ + data: ResourceDataType; + }>(String(permission.instanceId), { + accessTokenOverride: accessToken, + resource, + draft: true, + }); + + return { + resourceType: draftType, + data: response.data, + userId: user.id, + } as ResourceDraft; + } catch { + return null; + } + }); + + const results = await Promise.all(draftPromises); + return results.filter((draft) => draft !== null); +} + +export const fetchDrafts = async () => { + const authState = await getAuthStateServer(); + if (authState == null) { + return []; + } + + const { user, accessToken } = authState; + + if (isSolvroAdmin(user) || isAdmin(user)) { + return await fetchDraftsForAdmins(accessToken); + } + return await fetchUserOwnedDrafts(user, accessToken); +}; diff --git a/src/features/review/components/approve-button.tsx b/src/features/review/components/approve-button.tsx new file mode 100644 index 00000000..5a733497 --- /dev/null +++ b/src/features/review/components/approve-button.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { Check } from "lucide-react"; +import type { Route } from "next"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { fetchMutation, useMutationWrapper } from "@/features/backend"; +import { declineNoun } from "@/features/polish"; +import type { Resource } from "@/features/resources"; +import type { ResourcePk } from "@/features/resources/types"; +import { useRouter } from "@/hooks/use-router"; +import { sanitizeId } from "@/utils"; + +export function ApproveButton({ + id, + resource, +}: { + id: ResourcePk; + resource: Resource; + showLabel?: boolean; +}) { + const router = useRouter(); + + const { mutateAsync, isPending } = useMutationWrapper( + `approve-draft__${resource}__${String(id)}`, + async () => { + const result = await fetchMutation(`${sanitizeId(id)}/approve`, { + method: "POST", + resource, + draft: true, + }); + router.push(`/${resource}` as Route); + router.refresh(); + return result; + }, + ); + + const declensions = declineNoun(resource); + + //TODO: dialog? + return ( + + ); +} diff --git a/src/features/review/components/draft-edit-button.tsx b/src/features/review/components/draft-edit-button.tsx new file mode 100644 index 00000000..d362a76d --- /dev/null +++ b/src/features/review/components/draft-edit-button.tsx @@ -0,0 +1,31 @@ +import { SquarePen } from "lucide-react"; +import type { Route } from "next"; + +import { Link } from "@/components/core/link"; +import { Button } from "@/components/ui/button"; +import { getResourceMetadata } from "@/features/resources"; +import type { EditableResource, ResourcePk } from "@/features/resources/types"; +import { sanitizeId } from "@/utils"; + +export function DraftEditButton({ + resource, + id, +}: { + resource: EditableResource; + id: ResourcePk; +}) { + const metadata = getResourceMetadata(resource); + const draftPath = metadata.apiDraftPath?.replaceAll("_", "-"); //TODO + + if (draftPath == null) { + return null; + } + + return ( + + ); +} diff --git a/src/features/review/components/draft-item.tsx b/src/features/review/components/draft-item.tsx new file mode 100644 index 00000000..338f8ed2 --- /dev/null +++ b/src/features/review/components/draft-item.tsx @@ -0,0 +1,45 @@ +import { ArlItem } from "@/features/abstract-resource-list"; +import { isAdmin, isSolvroAdmin } from "@/features/authentication"; +import type { AuthState } from "@/features/authentication/types"; + +import type { + DraftableResource, + DraftableResourceRelationMap, + ResourceDraft, +} from "../types/internal"; +import { getDraftResource } from "../utils/get-draft-resource"; +import { DraftEditButton } from "./draft-edit-button"; + +export function DraftItem({ + draft, + relatedResourcesMap, + authState, +}: { + draft: ResourceDraft; + relatedResourcesMap: DraftableResourceRelationMap; + authState: AuthState; +}) { + const resource = getDraftResource(draft); + const relatedResources = relatedResourcesMap[resource]; + if (relatedResources == null) { + throw new Error(`Relations not found for draft resource ${resource}`); + } + + const canEdit = + isSolvroAdmin(authState.user) || + isAdmin(authState.user) || + draft.userId === authState.user.id; + + return ( + + ) : null + } + /> + ); +} diff --git a/src/features/review/components/draft-list.tsx b/src/features/review/components/draft-list.tsx new file mode 100644 index 00000000..5aed44c2 --- /dev/null +++ b/src/features/review/components/draft-list.tsx @@ -0,0 +1,48 @@ +import { fetchRelatedResources } from "@/features/abstract-resource-form"; +import { getAuthStateServer } from "@/features/authentication/server"; +import { typedFromEntries } from "@/utils"; + +import { fetchDrafts } from "../api/fetch-drafts"; +import type { DraftableResourceRelationMap } from "../types/internal"; +import { getDraftResource } from "../utils/get-draft-resource"; +import { DraftItem } from "./draft-item"; + +export async function DraftList() { + const [drafts, authState] = await Promise.all([ + fetchDrafts(), + getAuthStateServer(), + ]); + + if (authState == null) { + return null; + } + + const resources = new Set(drafts.map((draft) => getDraftResource(draft))); + const relatedResourcesMap: DraftableResourceRelationMap = typedFromEntries( + await Promise.all( + [...resources].map(async (resource) => [ + resource, + await fetchRelatedResources(resource), + ]), + ), + ); + + if (drafts.length === 0) { + return ( +

Brak draftów

+ ); + } + + return ( +
    + {drafts.map((draft) => ( + + ))} +
+ ); +} diff --git a/src/features/review/data/draft-type-resources.ts b/src/features/review/data/draft-type-resources.ts new file mode 100644 index 00000000..7e14f67a --- /dev/null +++ b/src/features/review/data/draft-type-resources.ts @@ -0,0 +1,8 @@ +import { Resource } from "@/features/resources"; + +import type { DraftableResource } from "../types/internal"; + +export const DRAFT_TYPE_RESOURCES = { + article_draft: Resource.GuideArticles, + organization_draft: Resource.StudentOrganizations, +} as const satisfies Record; diff --git a/src/features/review/index.ts b/src/features/review/index.ts new file mode 100644 index 00000000..c729bec3 --- /dev/null +++ b/src/features/review/index.ts @@ -0,0 +1 @@ +export * from "./components/approve-button"; diff --git a/src/features/review/server.ts b/src/features/review/server.ts new file mode 100644 index 00000000..3a41863c --- /dev/null +++ b/src/features/review/server.ts @@ -0,0 +1 @@ +export * from "./components/draft-list"; diff --git a/src/features/review/types/internal.ts b/src/features/review/types/internal.ts new file mode 100644 index 00000000..124cfc44 --- /dev/null +++ b/src/features/review/types/internal.ts @@ -0,0 +1,28 @@ +import type { Resource } from "@/features/resources"; +import type { + ResourceDataType, + SpecificResourceMetadata, +} from "@/features/resources/types"; +import type { ResourceRelations } from "@/types/components"; +import type { Invert } from "@/types/helpers"; + +import type { DRAFT_TYPE_RESOURCES } from "../data/draft-type-resources"; + +export type DraftableResource = { + [R in Resource]: SpecificResourceMetadata extends { apiDraftPath: string } + ? R + : never; +}[Resource]; + +type ResourceDraftTypes = Invert; +export interface ResourceDraft< + T extends DraftableResource = DraftableResource, +> { + resourceType: ResourceDraftTypes[T]; + data: ResourceDataType; + userId: number; +} + +export type DraftableResourceRelationMap = Partial<{ + [R in DraftableResource]: ResourceRelations; +}>; diff --git a/src/features/review/utils/get-draft-resource.ts b/src/features/review/utils/get-draft-resource.ts new file mode 100644 index 00000000..e15f9b89 --- /dev/null +++ b/src/features/review/utils/get-draft-resource.ts @@ -0,0 +1,10 @@ +import { DRAFT_TYPE_RESOURCES } from "../data/draft-type-resources"; +import type { DraftableResource, ResourceDraft } from "../types/internal"; + +export const getDraftResource = ( + draft: ResourceDraft, +): R => { + const resource = DRAFT_TYPE_RESOURCES[draft.resourceType]; + // TODO: double cast should not be necessary + return resource as DraftableResource as R; +}; diff --git a/src/schemas/positive-integer-schema.ts b/src/schemas/positive-integer-schema.ts index 40910b7a..9fb591ee 100644 --- a/src/schemas/positive-integer-schema.ts +++ b/src/schemas/positive-integer-schema.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { FORM_ERROR_MESSAGES } from "@/data/form-error-messages"; -export const PositiveIntegerSchema = z +export const PositiveIntegerSchema = z.coerce .number() .int() .positive({ message: FORM_ERROR_MESSAGES.REQUIRED }); diff --git a/src/types/components.ts b/src/types/components.ts index 854b501f..b5c10303 100644 --- a/src/types/components.ts +++ b/src/types/components.ts @@ -19,6 +19,7 @@ export type WrapperProps = Readonly<{ export interface ResourceFormProps { resource: T; className?: string; + draft?: boolean; } export type SearchParameters = Record; diff --git a/src/types/helpers.ts b/src/types/helpers.ts index 74a48a40..4b3c5641 100644 --- a/src/types/helpers.ts +++ b/src/types/helpers.ts @@ -17,3 +17,7 @@ export type OptionalPromise = T | Promise; export type NonNullableValues = { [K in keyof T]: NonNullable; }; + +export type Invert> = { + [K in keyof T as T[K]]: K; +};