Skip to content
This repository has been archived by the owner on Aug 11, 2023. It is now read-only.

Commit

Permalink
feat: Invite links for group
Browse files Browse the repository at this point in the history
  • Loading branch information
C4RR0T02 committed Jul 5, 2023
1 parent 4f32b36 commit 44b805d
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 34 deletions.
56 changes: 55 additions & 1 deletion composables/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const useGroupStore = defineStore('group', () => {
const { $api } = useNuxtApp()

const groups = ref<Group[]>([])
const invites = ref<Record<string, GroupInviteLink[]>>({})

/**
* init populates the store with groups available in iNProve and does nothing
Expand Down Expand Up @@ -57,13 +58,14 @@ export const useGroupStore = defineStore('group', () => {
/**
* update a group
*
* @param prevName name of the previous group to display current information presented on the site
* @param prevshortName short name of the previous group used to update all information
* @param name name of the group
* @param shortName short name of the group, must be alphanumeric (incl. dashes)
* @param description description of group
* @returns ValidationError | undefined
*/
async function update(prevshortName: string, name: string, shortName: string, description: string) {
async function update(prevName: string, prevshortName: string, name: string, shortName: string, description: string) {
try {
const res = await $api<Group>(`/groups/${prevshortName}`, {
method: 'PUT',
Expand Down Expand Up @@ -106,13 +108,65 @@ export const useGroupStore = defineStore('group', () => {
}
}

/**
* loadInvites populates the invites value on demand for a given group
*
* @param shortName short name of the group
* @returns ValidationError | undefined
*/
async function loadInvites(shortName: string) {
try {
const res = await $api<GroupInviteLink[]>(`/groups/${shortName}/invites`)
invites.value[shortName] = res
}
catch (err) {
console.error('[composables/groups.ts] failed to load invites', err)
return parseError(err)
}
}

async function createInvite(shortName: string, role: string) {
try {
const res = await $api<GroupInviteLink>(`/groups/${shortName}/invites`, {
method: 'POST',
body: {
role,
},
})
invites.value[shortName].push(res)
}
catch (e) {
console.error('[composables/groups.ts] failed to create invite', e)
return parseError(e)
}
}

async function delInvite(shortName: string, code: string) {
try {
await $api<GroupInviteLink>(`/groups/${shortName}/invites/${code}`, {
method: 'DELETE',
})

invites.value[shortName] = invites.value[shortName].filter(v => v.code !== code)
}
catch (err) {
console.error('[composables/groups.ts] failed to create invite', err)
return parseError(err)
}
}

return {
groups,
invites,

init,
create,
update,
del,

loadInvites,
createInvite,
delInvite,
}
})

Expand Down
196 changes: 163 additions & 33 deletions pages/dashboard/admin/groups/[shortName].vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
<script setup lang="ts">
import TabPanel from 'primevue/tabpanel'
import TabView from 'primevue/tabview'
import { useToast } from 'primevue/usetoast'
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import Dialog from 'primevue/dialog'
import Toolbar from 'primevue/toolbar'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
import Dropdown from 'primevue/dropdown'
import Tag from 'primevue/tag'
import type { ValidationError } from '~/utils/error'
definePageMeta({
layout: 'app',
middleware: 'god-mode',
})
const route = useRoute()
const router = useRouter()
const toast = useToast()
const group = useGroupStore()
onMounted(async () => {
await group.loadInvites(route.params.shortName as string)
})
const invitesData = computed(() => {
return group.invites[route.params.shortName as string] ?? []
})
const groupData = computed(() => group.groups.find(grp => grp.shortName === router.currentRoute.value.params.shortName))
const initFormData = computed(() => {
if (!groupData.value) {
return {
ogName: '',
ogShortName: '',
name: '',
shortName: '',
Expand All @@ -27,6 +45,7 @@ const initFormData = computed(() => {
}
}
return {
ogName: groupData.value.name,
ogShortName: groupData.value.shortName,
...groupData.value,
isLoading: false,
Expand All @@ -35,6 +54,7 @@ const initFormData = computed(() => {
})
const formData = ref<{
ogName: string
ogShortName: string
name: string
shortName: string
Expand All @@ -44,7 +64,7 @@ const formData = ref<{
}>(initFormData.value)
async function update() {
const err = await group.update(formData.value.ogShortName, formData.value.name, formData.value.shortName, formData.value.description)
const err = await group.update(formData.value.ogName, formData.value.ogShortName, formData.value.name, formData.value.shortName, formData.value.description)
if (err) {
formData.value.error = err
}
Expand Down Expand Up @@ -72,6 +92,59 @@ async function del() {
navigateTo('/dashboard/admin/groups')
}
}
// Invite Links
const roleTagMapping: Record<GroupRoles, string> = {
owner: 'danger',
educator: 'warning',
member: 'primary',
}
const inviteFormData = reactive<{
dialog: boolean
role?: InstitutionRoles
error?: ValidationError
pending: boolean
pendingDel?: string
}>({
dialog: false,
error: {},
pending: false,
})
const options = ref([
{ name: 'Owner', code: 'owner' },
{ name: 'Educator', code: 'educator' },
{ name: 'Member', code: 'member' },
])
async function createlink() {
if (!inviteFormData.role)
return
inviteFormData.pending = true
const err = await group.createInvite(groupData, inviteFormData.role)
if (err)
inviteFormData.error = err
inviteFormData.pending = false
inviteFormData.dialog = false
}
function copyLink(code: string) {
navigator.clipboard.writeText(`${window.location.origin}/s/${code}`)
}
async function delinvite(code: string) {
inviteFormData.pendingDel = code
const err = await group.delInvite(groupData, code)
if (err)
inviteFormData.error = err
delete inviteFormData.pendingDel
}
</script>

<template>
Expand All @@ -85,49 +158,106 @@ async function del() {
<div v-else>
<h3 text-lg font-semibold>
<span>
Edit {{ initFormData.name }}
Edit {{ initFormData.ogName }} Group
</span>
</h3>
<TabView>
<TabPanel header="Info">
<form mt-5 @submit.prevent="update">
<div flex flex-col gap-2>
<div>
<span class="p-float-label">
<InputText id="name" v-model="formData.name" type="text" required class="w-full" />
<label for="name">Name</label>
</span>
<small class="p-error">{{ formData.error?.fields?.name || '&nbsp;' }}</small>
</div>

<div mt-5>
<form @submit.prevent="update">
<div flex flex-col gap-2>
<div>
<span class="p-float-label">
<InputText id="name" v-model="formData.name" type="text" required class="w-full" />
<label for="name">Name</label>
</span>
<small class="p-error">{{ formData.error?.fields?.name || '&nbsp;' }}</small>
</div>
<div>
<span class="p-float-label">
<InputText id="shortName" v-model="formData.shortName" type="text" required class="w-full" />
<label for="shortName">Short name</label>
</span>
<small class="p-error">{{ formData.error?.fields?.shortName || '&nbsp;' }}</small>
</div>

<div>
<span class="p-float-label">
<InputText id="shortName" v-model="formData.shortName" type="text" required class="w-full" />
<label for="shortName">Short name</label>
</span>
<small class="p-error">{{ formData.error?.fields?.shortName || '&nbsp;' }}</small>
</div>
<div>
<span class="p-float-label">
<InputText id="description" v-model="formData.description" type="text" required class="w-full" />
<label for="description">Description</label>
</span>
<small class="p-error">{{ formData.error?.fields?.description || '&nbsp;' }}</small>
</div>

<div flex gap-3>
<Button type="submit">
Save changes
</Button>

<div>
<span class="p-float-label">
<InputText id="description" v-model="formData.description" type="text" required class="w-full" />
<label for="description">Description</label>
</span>
<small class="p-error">{{ formData.error?.fields?.description || '&nbsp;' }}</small>
<Button label="Delete" severity="danger" text @click="del">
Delete
</Button>
</div>
</div>
</form>
</TabPanel>
<TabPanel header="Invites">
<Dialog v-model:visible="inviteFormData.dialog" :modal="true" header="New invite">
<Dropdown
v-model="inviteFormData.role"
option-value="code"
editable
:options="options" option-label="name" placeholder="Select a role" class="w-full"
/>

<div flex gap-3>
<Button type="submit">
Save changes
<template #footer>
<Button text type="button" :loading="inviteFormData.pending" @click="createlink">
Create
</Button>
</template>
</Dialog>

<Button label="Delete" severity="danger" text @click="del">
Delete
</Button>
<Toolbar class="mb-4">
<template #end>
<Button label="New" severity="success" class="mr-2" @click="inviteFormData.dialog = true" />
</template>
</Toolbar>

<div v-if="invitesData.length === 0">
<div flex justify-center>
<span>No invites available!</span>
</div>
</div>
</form>
</div>

<DataTable v-else :value="invitesData">
<Column field="id" header="ID" />
<Column field="role" header="Role">
<template #body="{ data }">
<Tag :severity="roleTagMapping[data.role as GroupRoles]">
{{ data.role }}
</Tag>
</template>
</Column>
<Column field="code" header="Code">
<template #body="{ data }">
<span font-mono>{{ data.code }}</span>
</template>
</Column>
<Column field="code" header="Actions">
<template #body="{ data }">
<div flex gap-3>
<Button v-tooltip.bottom="'Copy link'" icon="true" text @click="copyLink(data.code)">
<div i-tabler-copy />
</Button>
<Button icon="true" severity="danger" text :loading="inviteFormData.pendingDel === data.code" @click="delinvite(data.code)">
<div i-tabler-trash />
</Button>
</div>
</template>
</Column>
</DataTable>
</TabPanel>
</TabView>
</div>
</div>
</template>
9 changes: 9 additions & 0 deletions utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,12 @@ interface Group {
shortName: string
description: string
}

interface GroupInviteLink {
id: string
code: string
role: GroupRoles
group: Group
}

type GroupRoles = 'owner' | 'educator' | 'member'

0 comments on commit 44b805d

Please sign in to comment.