Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(edit): move board between organizations and personal boards #1537

Merged
merged 13 commits into from
Jun 21, 2024
2 changes: 1 addition & 1 deletion next-tavla/app/(admin)/components/CreateBoard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ function NameAndOrganizationSelector({
</Checkbox>
<HiddenInput
id="organization"
value={selectedOrganization?.value}
value={selectedOrganization?.value.id}
/>
<div className="flex flex-row justify-end mt-8 ">
<PrimaryButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import {
hasBoardEditorAccess,
initializeAdminApp,
} from 'app/(admin)/utils/firebase'
import { firestore } from 'firebase-admin'
import admin, { firestore } from 'firebase-admin'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { TFontSize, TLocation } from 'types/meta'
import { TBoard, TBoardID } from 'types/settings'
import { TBoard, TBoardID, TOrganizationID } from 'types/settings'
import { getBoard, getWalkingDistanceTile } from '../../actions'
import { getUserFromSessionCookie } from 'app/(admin)/utils/server'

initializeAdminApp()

Expand Down Expand Up @@ -64,3 +65,67 @@ async function getTilesWithDistance(board: TBoard, location?: TLocation) {
}),
)
}

export async function moveBoardToPersonal(
bid: TBoardID,
from?: TOrganizationID,
) {
const user = await getUserFromSessionCookie()
if (!user) return getFormFeedbackForError('auth/operation-not-allowed')

if (from) {
const orgAccess = await hasBoardEditorAccess(bid)
if (!orgAccess) return redirect('/')
}
oyvindgrutle marked this conversation as resolved.
Show resolved Hide resolved

firestore()
.collection('users')
.doc(String(user.uid))
.update({
['owner']: admin.firestore.FieldValue.arrayUnion(bid),
})

firestore()
.collection(from ? 'organizations' : 'users')
.doc(from ? String(from) : String(user.uid))
.update({
[from ? 'boards' : 'owner']:
admin.firestore.FieldValue.arrayRemove(bid),
})

revalidatePath(`/edit/${bid}`)
}

export async function moveBoardToOrganization(
bid: TBoardID,
oid?: TOrganizationID,
from?: TOrganizationID,
) {
const user = await getUserFromSessionCookie()
if (!user) return getFormFeedbackForError('auth/operation-not-allowed')

const orgAccess = await hasBoardEditorAccess(bid)
if (!orgAccess) return redirect('/')

if (from) {
const orgAccess = await hasBoardEditorAccess(bid)
if (!orgAccess) return redirect('/')
}
oyvindgrutle marked this conversation as resolved.
Show resolved Hide resolved

firestore()
.collection('organizations')
.doc(String(oid))
.update({
['boards']: admin.firestore.FieldValue.arrayUnion(bid),
})

firestore()
.collection(from ? 'organizations' : 'users')
.doc(from ? String(from) : String(user.uid))
.update({
[from ? 'boards' : 'owner']:
admin.firestore.FieldValue.arrayRemove(bid),
})

revalidatePath(`/edit/${bid}`)
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,46 @@
'use client'
import { TextField } from '@entur/form'
import { Checkbox, TextField } from '@entur/form'
import { Heading3 } from '@entur/typography'
import { TFontSize, TMeta } from 'types/meta'
import { saveFont, saveTitle } from './actions'
import { TBoardID } from 'types/settings'
import {
moveBoardToOrganization,
moveBoardToPersonal,
saveFont,
saveTitle,
} from './actions'
import { TBoardID, TOrganization } from 'types/settings'
import { FontChoiceChip } from './FontChoiceChip'
import { SubmitButton } from 'components/Form/SubmitButton'
import { Address } from './Adress'
import { DEFAULT_BOARD_NAME } from 'app/(admin)/utils/constants'
import { useToast } from '@entur/alert'
import { isEmptyOrSpaces } from 'app/(admin)/edit/utils'
import { Dropdown } from '@entur/dropdown'
import { useOrganizations } from 'app/(admin)/hooks/useOrganizations'
import { useState } from 'react'
import {
TFormFeedback,
getFormFeedbackForError,
getFormFeedbackForField,
} from 'app/(admin)/utils'

function MetaSettings({ bid, meta }: { bid: TBoardID; meta?: TMeta }) {
function MetaSettings({
bid,
meta,

organization,
}: {
bid: TBoardID
meta: TMeta
organization?: TOrganization
}) {
const { addToast } = useToast()
const { organizations, selectedOrganization, setSelectedOrganization } =
useOrganizations(organization)
const [personal, setPersonal] = useState<boolean>(
oyvindgrutle marked this conversation as resolved.
Show resolved Hide resolved
organization ? false : true,
)
const [state, setFormError] = useState<TFormFeedback | undefined>()
return (
<>
<form
Expand Down Expand Up @@ -63,6 +91,54 @@ function MetaSettings({ bid, meta }: { bid: TBoardID; meta?: TMeta }) {
</SubmitButton>
</div>
</form>
<form
action={async () => {
if (!selectedOrganization && !personal) {
return setFormError(
getFormFeedbackForError(
'create/organization-missing',
),
)
}

personal
? await moveBoardToPersonal(bid, organization?.id)
: await moveBoardToOrganization(
bid,
selectedOrganization?.value.id,
organization?.id,
)

setFormError(undefined)
addToast('Organisasjon lagret!')
}}
className="box flex flex-col"
>
<Heading3 margin="bottom">Organisasjon</Heading3>
<Dropdown
items={organizations}
label="Dine organisasjoner"
selectedItem={selectedOrganization}
onChange={setSelectedOrganization}
clearable
className="mb-4"
aria-required="true"
disabled={personal}
{...getFormFeedbackForField('organization', state)}
/>
<Checkbox
checked={personal}
onChange={() => setPersonal(!personal)}
name="personal"
>
Privat tavle
</Checkbox>
<div className="flex flex-row mt-8 justify-end">
<SubmitButton variant="secondary" className="max-sm:w-full">
Lagre organisasjon
</SubmitButton>
oyvindgrutle marked this conversation as resolved.
Show resolved Hide resolved
</div>
</form>
</>
)
}
Expand Down
8 changes: 6 additions & 2 deletions next-tavla/app/(admin)/edit/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,12 @@ export default async function EditPage({ params }: TProps) {
<RefreshButton board={board} />
</div>
</div>
<div className="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-8">
<MetaSettings bid={params.id} meta={board.meta} />
<div className="grid grid-cols-[repeat(auto-fill,minmax(400px,1fr))] gap-8">
<MetaSettings
bid={params.id}
meta={board.meta}
organization={organization}
/>
<Footer
bid={params.id}
footer={board.footer}
Expand Down
10 changes: 10 additions & 0 deletions next-tavla/app/(admin)/edit/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getTransportIcon } from 'components/TransportIcon'
import { uniq } from 'lodash'
import { TTransportMode } from 'types/graphql-schema'
import { TLocation } from 'types/meta'
import { TOrganization } from 'types/settings'

export type TCategory =
| 'onstreetBus'
Expand Down Expand Up @@ -31,6 +32,15 @@ export function locationToDropdownItem(
}
}

export function organizationToDropdownItem(
organization: TOrganization,
): NormalizedDropdownItemType<TOrganization> {
return {
label: organization.name ?? '',
value: organization ?? undefined,
}
}

export function categoryToTransportmode(category: TCategory): TTransportMode {
switch (category) {
case 'onstreetBus':
Expand Down
15 changes: 11 additions & 4 deletions next-tavla/app/(admin)/hooks/useOrganizations.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { NormalizedDropdownItemType } from '@entur/dropdown'
import { useCallback, useEffect, useState } from 'react'
import { getOrganizationsForUser } from '../actions'
import { TOrganization } from 'types/settings'
import { organizationToDropdownItem } from '../edit/utils'

function useOrganizations() {
function useOrganizations(organization?: TOrganization) {
const [organizationList, setOrganizationList] = useState<
NormalizedDropdownItemType<string>[]
NormalizedDropdownItemType<TOrganization>[]
>([])
const [selectedOrganization, setSelectedOrganization] =
useState<NormalizedDropdownItemType | null>(null)
useState<NormalizedDropdownItemType<TOrganization> | null>(
organization ? organizationToDropdownItem(organization) : null,
)

useEffect(() => {
getOrganizationsForUser().then((res) => {
setOrganizationList(
res?.map((o) => ({ value: o.id ?? '', label: o.name ?? '' })),
res?.map((o) => ({
value: o ?? undefined,
label: o.name ?? '',
})),
)
})
}, [])
Expand Down