Skip to content

Commit

Permalink
Add manage tester in group
Browse files Browse the repository at this point in the history
  • Loading branch information
yunusefendi52 committed Nov 27, 2024
1 parent 827fc39 commit 9854d60
Show file tree
Hide file tree
Showing 22 changed files with 2,237 additions and 27 deletions.
2 changes: 2 additions & 0 deletions assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ body {
--p-tabmenu-active-bar-height: 2.5px;
--p-content-border-color: var(--p-datatable-border-color);
--p-tabmenu-item-color: var(--p-text-color);

word-break: break-word;
}

.p-datatable-table-container {
Expand Down
99 changes: 99 additions & 0 deletions components/ManageAppTester.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<template>
<Drawer v-model:visible="isVisible" position="right" class="!w-[28rem]" header="Manage Testers">
<div class="flex flex-col gap-3">
<AppCard class="flex flex-col gap-1 justify-stretch !p-2">
<form @submit.prevent="mutate">
<div class="flex flex-row gap-2">
<InputText class="flex-1" name="email" placeholder="Email" type="email" size="small" />
<Button label="Create Invitation" size="small" :loading="isPending" type="submit" />
</div>
<div class="flex flex-row gap-2 mt-2 items-center" v-if="data">
<span class="flex-1 text-sm">Link invitation created</span>
<Button label="Click to copy" size="small" outlined @click="() => copyLink(data)" />
</div>
</form>
</AppCard>
<div class="flex flex-col gap-3">
<template v-if="testers.length">
<AppCard v-for="tester in testers" class="!p-0">
<div class="p-3 flex flex-row gap-2 items-center">
<span class="flex-1">{{ tester.users?.email }}</span>
<Button icon="pi pi-trash" severity="danger" outlined @click="() => removeTester({
userId: tester.users!.id!,
})" :loading="removeStatus === 'pending'" />
</div>
</AppCard>
</template>
<div v-else class="mt-1">
<span>No tester yet</span>
</div>
</div>
</div>
</Drawer>
</template>

<script setup lang="ts">
import { copyText } from '#imports'
const isVisible = defineModel<boolean>()
const { params: { appId, orgName, detailGroup: groupName } } = useRoute()
const { data: listTester, status: listTesterStatus, execute: getListTester } = useFetch('/api/apps/testers/list-tester', {
method: 'get',
query: {
orgName,
appName: appId,
groupName,
},
})
const testers = computed(() => listTester.value?.testers ?? [])
watchEffect(() => {
if (isVisible.value) {
getListTester()
}
})
const { mutate, isPending, data } = useMutation({
async mutationFn(r: any) {
const request = getObjectForm(r)
return await $fetch.raw('/api/apps/testers/create-invitation-tester', {
method: 'post',
body: {
orgName,
appName: appId,
groupName,
...request,
},
onResponseError(error) {
showErrorAlert(error)
},
}).then(e => e._data?.testerInviteLink)
},
})
function copyLink(value: string | undefined) {
const link = `${window.location.origin}/install/join-tester#${value}`
copyText(link)
}
const { mutate: removeTester, status: removeStatus } = useMutation({
mutationFn: async (r: { userId: string }) => {
if (confirm('Do you want remove this tester?')) {
await $fetch.raw('/api/apps/testers/remove-tester', {
method: 'post',
body: {
orgName,
appName: appId,
groupName,
...r,
},
onResponse() {
getListTester()
},
onResponseError(error) {
showErrorAlert(error)
}
})
}
},
})
</script>
16 changes: 16 additions & 0 deletions layouts/install-layout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<template>
<div class="flex flex-row gap-1 items-center">
<AppHeader class="flex-auto" />
<div class="flex flex-row gap-2 px-3">
<Button v-if="cookie" label="Sign Out" outlined />
<Button v-else label="Sign In" outlined />
</div>
</div>
<div class="flex max-w-lg flex-col mx-auto justify-stretch sm:px-0 pt-6 pb-20">
<slot />
</div>
</template>

<script setup lang="ts">
const cookie = useCookie('app-auth')
</script>
20 changes: 9 additions & 11 deletions pages/install/[publicId]/index.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
<template>
<div class="flex flex-row gap-1 items-center">
<AppHeader class="flex-auto" />
<div class="flex flex-row gap-2 px-3">
<Button v-if="cookie" label="Sign Out" outlined />
<Button v-else label="Sign In" outlined />
</div>
</div>
<div class="flex flex-col items-center">
<div v-if="status === 'pending'">
<ProgressSpinner style="width: 50px; height: 50px; margin: unset;" strokeWidth="6" />
</div>
<div class="container flex flex-col gap-5 p-5" v-else>
<div v-else-if="status === 'error' && error">
<span>{{ normalizeError(error) }}</span>
</div>
<div class="container flex flex-col gap-5 px-5" v-else>
<div class="flex flex-col justify-start gap-3">
<span class=" text-4xl">{{ data?.app.displayName }}</span>
<div class="flex justify-start gap-2">
Expand All @@ -19,7 +15,7 @@
<Badge :value="data?.artifactGroup.name" severity="info" />
</div>
</div>
<Panel v-for="item in data?.artifacts.map(e => e.artifacts!)" :key="item.id">
<Panel v-for="item in artifacts" :key="item.id">
<template #header>
<div class="flex flex-row w-full">
<div class="flex-1 flex flex-col gap-1">
Expand Down Expand Up @@ -53,24 +49,26 @@

<script setup lang="ts">
import { formatDate } from '#imports'
import { normalizeError } from '~/utils/showErrorAlert'
definePageMeta({
layout: false,
layout: 'install-layout',
server: false,
path: '/install/:orgName/apps/:appName/:publicId',
})
const route = useRoute()
const { value: { publicId, orgName, appName } } = computed(() => route.params)
const { data, status } = useFetch('/api/install/get-data', {
const { data, status, error } = useFetch('/api/install/get-data', {
query: {
publicId,
orgName,
appName,
},
server: false,
})
const artifacts = computed(() => data.value?.artifacts.map(e => e.artifacts!))
useSeoMeta({
title: `DistApp - ${data.value?.app.name ?? ''}`,
Expand Down
67 changes: 67 additions & 0 deletions pages/install/join-tester.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<template>
<div v-if="status === 'pending'">
<ProgressSpinner style="width: 40px; height: 40px; margin: unset;" strokeWidth="4" />
</div>
<div v-if="status === 'error'">
<span>{{ error?.data?.message }}</span>
</div>
<div class="flex flex-col" v-else>
<span class="text-lg">You are invited to join tester "{{ data?.group?.displayName }}"</span>
<span class="text-2xl mt-3">{{ data?.app.displayName }}</span>
<span class="">{{ data?.org.displayName }}</span>
<Button label="Join Tester" class="mt-5" :loading="isPending" @click="mutate" />
</div>
</template>

<script setup lang="ts">
definePageMeta({
layout: 'install-layout',
server: false,
})
useSeoMeta({
title: `DistApp - Join Tester`,
})
const { hash } = useRoute()
const router = useRouter()
const testerToken = computed(() => hash.slice(1))
const { status, data, execute, error } = useFetch('/api/install/tester/get-data-join-tester', {
method: 'get',
query: {
token: testerToken.value,
},
immediate: true,
})
onMounted(() => {
if (!import.meta.dev) {
router.replace({
hash: '',
replace: true,
})
}
})
const { mutate, isPending } = useMutation({
async mutationFn(r: any) {
const { _data } = await $fetch.raw('/api/install/tester/join-tester', {
method: 'post',
body: {
token: testerToken.value,
},
onResponseError(error) {
showErrorAlert(error)
},
})
if (_data) {
navigateTo({
name: 'install-publicId',
params: {
orgName: _data.orgName,
appName: _data.appName,
publicId: _data.groupName,
},
})
}
},
})
</script>
4 changes: 4 additions & 0 deletions pages/orgs/[orgName]/apps/[appId]/groups/[detailGroup].vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
publicLink
}}</a> </span>
</div>
<Button icon="pi pi-user" @click="() => showManageApptester = true" />
<Button icon="pi pi-pencil" @click="() => groupSettings = true" />
<!-- <Button icon="pi pi-refresh" label="Regenerate Link" @click="regenerateLink" /> -->
</div>
Expand All @@ -31,6 +32,8 @@
<Releases :org-name="orgName" :app-name="appName" :os-type="'android'" :group-name="groupName" />
</div>

<LazyManageAppTester v-model="showManageApptester" />

<Drawer v-model:visible="groupSettings" header="Group Settings" position="right" class="!w-[24rem]">
<form @submit.prevent="mutateArtifactGroupData">
<div class="flex flex-col gap-3 items-stretch">
Expand Down Expand Up @@ -147,6 +150,7 @@ const removeGroup = (event: any) => {
}
const groupSettings = ref(false)
const showManageApptester = ref(false)
const isPublic = ref(detailGroup.value?.isPublic ?? false)
watchEffect(() => {
Expand Down
33 changes: 33 additions & 0 deletions server/api/apps/testers/create-invitation-tester.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { JoinTesterPayload } from "~/server/models/JoinTesterPayload"

export default defineEventHandler(async (event) => {
var { orgName, appName, groupName, email } = await readValidatedBody(event, z.object({
orgName: z.string().max(128),
appName: z.string().max(128),
groupName: z.string().max(128),
email: z.string().email(),
}).parse)
email = email.trim()

if (await roleEditNotAllowed(event, orgName)) {
throw createError({
message: 'Unauthorized',
statusCode: 401,
})
}
const { app: { apiAuthKey } } = useRuntimeConfig(event)
const { userApp, userOrg } = await getUserApp(event, orgName, appName)
const testerInviteLink = await generateTokenWithOptions(event, {
email: email,
orgName: userOrg.org!.name,
orgId: userOrg.org!.id,
appName: userApp.name,
appId: userApp.id,
groupName,
} satisfies JoinTesterPayload, (v) => {
return v.setExpirationTime('24 hour')
}, apiAuthKey!)
return {
testerInviteLink,
}
})
29 changes: 29 additions & 0 deletions server/api/apps/testers/list-tester.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export default defineEventHandler(async (event) => {
var { orgName, appName, groupName } = await getValidatedQuery(event, z.object({
orgName: z.string().max(128),
appName: z.string().max(128),
groupName: z.string().max(128),
}).parse)

const db = event.context.drizzle
const { userApp, userOrg } = await getUserApp(event, orgName, appName)
const testers = await db.select({
groupTester: getTableColumns(tables.groupTester),
artifactsGroups: getTableColumns(tables.artifactsGroups),
users: {
id: tables.users.id,
email: tables.users.email,
},
})
.from(tables.groupTester)
.leftJoin(tables.artifactsGroups, eq(tables.artifactsGroups.id, tables.groupTester.artifactGroupId))
.leftJoin(tables.users, eq(tables.users.id, tables.groupTester.testerId))
.where(and(
eq(tables.groupTester.organizationId, userOrg.org!.id!),
eq(tables.groupTester.appsId, userApp.id),
eq(tables.artifactsGroups.name, groupName),
))
return {
testers,
}
})
34 changes: 34 additions & 0 deletions server/api/apps/testers/remove-tester.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { JoinTesterPayload } from "~/server/models/JoinTesterPayload"

export default defineEventHandler(async (event) => {
var { orgName, appName, groupName, userId } = await readValidatedBody(event, z.object({
orgName: z.string().max(128),
appName: z.string().max(128),
groupName: z.string().max(128),
userId: z.string().min(1),
}).parse)

if (await roleEditNotAllowed(event, orgName)) {
throw createError({
message: 'Unauthorized',
statusCode: 401,
})
}
const { userApp, userOrg } = await getUserApp(event, orgName, appName)
const db = event.context.drizzle
const artifactGroup = await db.select({
groupId: tables.artifactsGroups.id,
}).from(tables.artifactsGroups)
.where(eq(tables.artifactsGroups.name, groupName))
.then(takeUniqueOrThrow)
await db.delete(tables.groupTester)
.where(and(
eq(tables.groupTester.testerId, userId),
eq(tables.groupTester.organizationId, userOrg.org!.id),
eq(tables.groupTester.appsId, userApp.id),
eq(tables.groupTester.artifactGroupId, artifactGroup.groupId),
))
return {
ok: true,
}
})
Loading

0 comments on commit 9854d60

Please sign in to comment.