Skip to content

Commit

Permalink
Create new project (partial)
Browse files Browse the repository at this point in the history
Creates a new project locally.
For some reason, BuildEngine is unhappy, even though it is allegedly receiving the exact same information from S1.
  • Loading branch information
FyreByrd committed Oct 4, 2024
1 parent 1858990 commit 00e7ec5
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 4 deletions.
6 changes: 3 additions & 3 deletions source/SIL.AppBuilder.Portal/common/databaseProxy/Projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ import type { RequirePrimitive } from './utility.js';

export async function create(
projectData: RequirePrimitive<Prisma.ProjectsUncheckedCreateInput>
): Promise<boolean> {
): Promise<boolean | number> {
if (!validateProjectBase(projectData.OrganizationId, projectData.GroupId, projectData.OwnerId))
return false;

// No additional verification steps

try {
await prisma.projects.create({
const res = await prisma.projects.create({
data: projectData
});
return res.Id;
} catch (e) {
return false;
}
return true;
}

export async function update(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ export class CheckBuildProduct extends ScriptoriaJobExecutor<BullMQ.ScriptoriaJo
// TODO: what does the 'expired' status mean?
if (response.status === 'completed' || response.status === 'expired') {
await scriptoriaQueue.removeRepeatableByKey(job.repeatJobKey);
if (response.error) {
job.log(response.error);
}
const flow = await Workflow.restore(job.data.productId);
if (response.result === 'SUCCESS') {
flow.send({ type: 'Build Successful', userId: null });
Expand Down Expand Up @@ -227,6 +230,9 @@ export class CheckPublishProduct extends ScriptoriaJobExecutor<BullMQ.Scriptoria
// TODO: what does the 'expired' status mean?
if (response.status === 'completed' || response.status === 'expired') {
await scriptoriaQueue.removeRepeatableByKey(job.repeatJobKey);
if (response.error) {
job.log(response.error);
}
const flow = await Workflow.restore(job.data.productId);
if (response.result === 'SUCCESS') {
flow.send({ type: 'Publish Successful', userId: null });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Job } from 'bullmq';
import { ScriptoriaJobExecutor } from './base.js';

// TODO: What would be a meaningful return?
// TODO: Figure out why this causes errors in BuildEngine but S1 does not
export class CreateProject extends ScriptoriaJobExecutor<BullMQ.ScriptoriaJobType.CreateProject> {
async execute(job: Job<BullMQ.CreateProjectJob, number, string>): Promise<number> {
const projectData = await prisma.projects.findUnique({
Expand Down Expand Up @@ -41,6 +42,8 @@ export class CreateProject extends ScriptoriaJobExecutor<BullMQ.ScriptoriaJobTyp
} else {
await DatabaseWrites.projects.update(job.data.projectId, {
WorkflowProjectId: response.id,
WorkflowProjectUrl: response.url,
WorkflowAppProjectUrl: `${process.env.UI_URL ?? 'http://localhost:5173'}/projects/${job.data.projectId}`,
DateUpdated: new Date().toString()
});
job.updateProgress(75);
Expand Down Expand Up @@ -79,6 +82,9 @@ export class CheckCreateProject extends ScriptoriaJobExecutor<BullMQ.ScriptoriaJ
} else {
if (response.status === 'complete') {
await scriptoriaQueue.removeRepeatableByKey(job.repeatJobKey);
if (response.error) {
job.log(response.error);
}
job.updateProgress(100);
return response.id;
}
Expand Down
1 change: 1 addition & 0 deletions source/SIL.AppBuilder.Portal/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare module '@auth/sveltekit' {
interface Session {
user: {
userId: number;
/** [organizationId, RoleId][]*/
roles: [number, number][];
} & DefaultSession['user'];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<button class="btn btn-outline mx-1" on:click={() => alert('TODO api proxy')}>
{m.project_importProjects()}
</button>
<button class="btn btn-outline mx-1" on:click={() => alert('TODO api proxy')}>
<button class="btn btn-outline mx-1" on:click={() => goto(`/projects/new/${selectedOrg}`)}>
{m.sidebar_addProject()}
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { idSchema } from '$lib/valibot';
import { error } from '@sveltejs/kit';
import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common';
import { fail, superValidate } from 'sveltekit-superforms';
import { valibot } from 'sveltekit-superforms/adapters';
import * as v from 'valibot';
import type { Session } from '@auth/sveltekit';
import type { Actions, PageServerLoad } from './$types';
import { RoleId } from 'sil.appbuilder.portal.common/prisma';
import { scriptoriaQueue, BullMQ } from 'sil.appbuilder.portal.common';
import { time } from 'console';

const projectCreateSchema = v.object({
name: v.pipe(v.string(), v.minLength(1)),
group: idSchema,
language: v.pipe(v.string(), v.minLength(1)),
type: idSchema,
description: v.nullable(v.string()),
public: v.boolean()
});

export const load = (async ({ locals, params }) => {
if (!verifyCanCreateProject((await locals.auth())!, parseInt(params.id))) return error(403);

const organization = await prisma.organizations.findUnique({
where: {
Id: parseInt(params.id)
},
select: {
Groups: {
select: {
Id: true,
Name: true
}
},
OrganizationProductDefinitions: {
select: {
ProductDefinition: {
select: {
ApplicationTypes: true
}
}
}
},
PublicByDefault: true
}
});

const types = organization?.OrganizationProductDefinitions.map(
(opd) => opd.ProductDefinition.ApplicationTypes
).reduce((p, c) => {
if (!p.some((e) => e.Id === c.Id)) {
p.push(c);
}
return p;
}, [] as { Id: number; Name: string | null; Description: string | null }[]);

const form = await superValidate(
{
group: organization?.Groups[0]?.Id ?? undefined,
type: types?.[0].Id ?? undefined,
public: organization?.PublicByDefault ?? undefined
},
valibot(projectCreateSchema)
);
return { form, organization, types };
}) satisfies PageServerLoad;

export const actions: Actions = {
default: async function (event) {
const session = (await event.locals.auth())!;
if (!verifyCanCreateProject(session, parseInt(event.params.id))) return error(403);

const form = await superValidate(event.request, valibot(projectCreateSchema));
// TODO: Return/Display error messages
if (!form.valid) return fail(400, { form, ok: false });
if (isNaN(parseInt(event.params.id))) return fail(400, { form, ok: false });
const timestamp = (new Date()).toString();
const project = await DatabaseWrites.projects.create({
OrganizationId: parseInt(event.params.id),
Name: form.data.name,
GroupId: form.data.group,
OwnerId: session.user.userId,
Language: form.data.language,
TypeId: form.data.type,
Description: form.data.description ?? '',
DateCreated: timestamp,
DateUpdated: timestamp
// TODO: DateActive?
});

if (project !== false) {
scriptoriaQueue.add(`Create Project #${project}`, {
type: BullMQ.ScriptoriaJobType.CreateProject,
projectId: project as number
});
}

return { form, ok: project !== false };
}
};

async function verifyCanCreateProject(user: Session, orgId: number) {
// Creating a project is allowed if the user is an OrgAdmin, AppBuilder, or SuperAdmin for the organization
const roles = user.user.roles.filter(([org, role]) => org === orgId).map(([org, role]) => role);
return (
roles.includes(RoleId.AppBuilder) ||
roles.includes(RoleId.OrgAdmin) ||
roles.includes(RoleId.SuperAdmin)
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import LanguageCodeTypeahead from '$lib/components/LanguageCodeTypeahead.svelte';
import * as m from '$lib/paraglide/messages';
import { superForm } from 'sveltekit-superforms';
import type { PageData } from './$types';
export let data: PageData;
const { form, enhance } = superForm(data.form, {
dataType: 'json',
onUpdated(event) {
if (event.form.valid) {
goto('/projects/' + $page.params.id);
}
}
});
</script>

<div class="w-full max-w-6xl mx-auto relative p-2">
<form action="" method="post" use:enhance>
<h1>{m.project_newProject()}</h1>
<div class="grid gap-2 gap-x-8 gridcont">
<div class="w-full flex place-content-between">
<label for="name">{m.project_projectName()}:</label>
<input type="text" id="name" class="input input-bordered" bind:value={$form.name} />
</div>
<div class="w-full flex place-content-between">
<label for="group">{m.project_projectGroup()}:</label>
<select name="group" id="group" class="select select-bordered" bind:value={$form.group}>
{#each data.organization?.Groups ?? [] as group}
<option value={group.Id}>{group.Name}</option>
{/each}
</select>
</div>
<div class="w-full flex place-content-between">
<label for="language">{m.project_languageCode()}:</label>
<!-- <input type="text" id="language" class="input input-bordered" bind:value={$form.language} /> -->
<LanguageCodeTypeahead bind:langCode={$form.language} dropdownClasses="right-0" />
</div>
<div class="w-full flex place-content-between">
<label for="type">{m.project_type()}:</label>
<select name="type" id="type" class="select select-bordered" bind:value={$form.type}>
{#each data.types ?? [] as type}
<option value={type.Id}>{type.Description}</option>
{/each}
</select>
</div>
<div class="mt-4">
<label for="description">
{m.project_projectDescription()}
<textarea
name="description"
id="description"
class="textarea textarea-bordered w-full"
bind:value={$form.description}
/>
</label>
</div>
<div class="w-full flex flex-col">
<div class="form-control">
<label for="public" class="label">{m.project_public()}:
<input type="checkbox" name="public" id="public" class="toggle" bind:checked={$form.public} />
</label>
</div>
<p>{m.project_visibilityDescription()}</p>
</div>
</div>
<div class="flex place-content-end space-x-2">
<a href="/projects/{$page.params.id}" class="btn">{m.common_cancel()}</a>
<button class="btn btn-primary" type="submit">{m.common_save()}</button>
</div>
</form>
</div>

<style>
.gridcont {
grid-template-columns: repeat(auto-fill, minmax(48%, 1fr));
}
.gridcont div.flex label {
margin-right: 0.4rem;
}
.label[for="public"] {
padding-left: 0px;
}
</style>

0 comments on commit 00e7ec5

Please sign in to comment.