Skip to content

Commit

Permalink
Feature/svelte task forms (#1016)
Browse files Browse the repository at this point in the history
* Import prisma in flows and auth

* Add structure for task forms

* Direct my tasks links to correct subfolder

* Add rest of instructions

* Use instructions dynamically in svelte:component

Also fixed some things to make TS happy.

* Make tables in forms sortable

* Add simple queries and log results

* Add comments noting which database field

* Retrieve product data from database

Currently grabbing all relevant fields.
Once workflow backend is implemented, should change to only grab data needed based on the state of the task in the workflow.

* Move arrow icons to lib/icons

* Rename fields to be more meaningful

* Rename instructions to align with DWKit forms

* Remove unneeded query

* Add note for ProductArtifacts

* Clean up code for SortTable and co.

* Break out artifacts query, filter on latest build

---------

Co-authored-by: 7dev7urandom <[email protected]>
  • Loading branch information
FyreByrd and 7dev7urandom authored Sep 6, 2024
1 parent eb9d160 commit 2307fae
Show file tree
Hide file tree
Showing 24 changed files with 766 additions and 2 deletions.
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 @@ -2,6 +2,7 @@ import { SvelteKitAuth, type DefaultSession, type SvelteKitAuthConfig } from '@a
import Auth0Provider from '@auth/sveltekit/providers/auth0';
import { redirect, type Handle } from '@sveltejs/kit';
import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common';
import { RoleId } from 'sil.appbuilder.portal.common/prisma';

declare module '@auth/sveltekit' {
interface Session {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- source: https://fonts.google.com/icons -->
<svg
class="inline fill-current"
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px">
<path d="M480-360 280-560h400L480-360Z"/>
</svg>
9 changes: 9 additions & 0 deletions source/SIL.AppBuilder.Portal/src/lib/icons/ArrowUpIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- source: https://fonts.google.com/icons -->
<svg
class="inline fill-current"
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px">
<path d="m280-400 200-200 200 200H280Z"/>
</svg>
4 changes: 3 additions & 1 deletion source/SIL.AppBuilder.Portal/src/lib/icons/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import ArrowBackIcon from './ArrowBackIcon.svelte';
import ArrowDownIcon from './ArrowDownIcon.svelte';
import HamburgerIcon from './HamburgerIcon.svelte';
import LanguageIcon from './LanguageIcon.svelte';
import ArrowUpIcon from './ArrowUpIcon.svelte';

export { ArrowBackIcon, HamburgerIcon, LanguageIcon };
export { ArrowBackIcon, ArrowDownIcon, HamburgerIcon, LanguageIcon, ArrowUpIcon };
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
class="cursor-pointer"
on:click={() =>
goto(
`/flow/${task.Product.ProductDefinition.Workflow.WorkflowBusinessFlow}/${task.ProductId}`
`/tasks/${task.Id}`
)}
>
<td>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { PageServerLoad } from './$types';
import { prisma } from 'sil.appbuilder.portal.common';

type Fields = {
ownerName?: string; //Product.Project.Owner.Name
ownerEmail?: string; //Product.Project.Owner.Email
projectName: string; //Product.Project.Name
projectDescription: string; //Product.Project.Description
storeDescription?: string; //Product.Store.Description
listingLanguageCode?: string; //Product.StoreLanguage.Name
projectURL?: string; //Product.Project.WorkflowAppProjectURL
productDescription?: string; //Product.ProductDefinition.Description
appType?: string; //Product.ProductDefinition.ApplicationTypes.Description
projectLanguageCode?: string; //Product.Project.Language
}

export const load = (async ({ params, url, locals }) => {
const product = await prisma.products.findUnique({
where: {
Id: params.product_id
},
select: {
WorkflowBuildId: true,
Project: {
select: {
Name: true,
Description: true,
WorkflowAppProjectUrl: true,
Language: true,
Owner: {
select: {
Name: true,
Email: true
}
},
Reviewers: {
select: {
Id: true,
Name: true,
Email: true
}
}
}
},
Store: {
select: {
Description: true
}
},
StoreLanguage: {
select: {
Name: true
}
},
ProductDefinition: {
select: {
Name: true,
ApplicationTypes: {
select: {
Description: true
}
}
}
},
}
});

// later: include only when workflow state needs it
const artifacts = await prisma.productArtifacts.findMany({
where: {
ProductId: params.product_id,
ProductBuild: {
BuildId: product?.WorkflowBuildId
}
// later: some forms don't need all artifacts, some just need aab
},
select: {
ProductBuildId: true,
ContentType: true,
FileSize: true,
Url: true,
Id: true
}
})

return {
actions: [],
taskTitle: "Waiting",
instructions: "waiting",
//filter fields/files/reviewers based on task once workflows are implemented
//possibly filter in the original query to increase database efficiency
fields: {
ownerName: product?.Project.Owner.Name,
ownerEmail: product?.Project.Owner.Email,
projectName: product?.Project.Name,
projectDescription: product?.Project.Description,
storeDescription: product?.Store?.Description,
listingLanguageCode: product?.StoreLanguage?.Name,
projectURL: product?.Project.WorkflowAppProjectUrl,
productDescription: product?.ProductDefinition.Name,
appType: product?.ProductDefinition.ApplicationTypes.Description,
projectLanguageCode: product?.Project.Language
} as Fields,
files: artifacts,
reviewers: product?.Project.Reviewers
}
}) satisfies PageServerLoad;
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<script lang="ts">
import type { PageData } from './$types';
import { instructions } from './instructions';
import SortTable from "./components/SortTable.svelte";
export let data: PageData;
</script>

<div class="p-5">
<form>
{#if data.actions?.length}
<div class="flex flex-row gap-x-3">
{#each data.actions as action}
<input type="submit" class="btn" value={action}>
{/each}
</div>
{/if}
<label class="form-control">
<div class="label">
<span class="label-text">Comment</span>
</div>
<textarea class="textarea textarea-bordered h-24"></textarea>
</label>
</form>
<hr class="border-t-4 my-2">
<h2>
{data.taskTitle}
</h2>
<div>
{#if data.fields.ownerName && data.fields.ownerEmail}
<div class="flex flex-col gap-x-3 w-full md:flex-row">
<label class="form-control w-full md:w-2/4">
<div class="label">
<span class="label-text">User Name</span>
</div>
<input type="text" class="input input-bordered w-full" readonly value={data.fields.ownerName} />
</label>
<label class="form-control w-full md:w-2/4">
<div class="label">
<span class="label-text">Email</span>
</div>
<input type="text" class="input input-bordered w-full" readonly value={data.fields.ownerEmail} />
</label>
</div>
{/if}
<div class="flex flex-col gap-x-3 w-full md:flex-row">
<label class="form-control w-full md:w-2/4">
<div class="label">
<span class="label-text">Project Name</span>
</div>
<input type="text" class="input input-bordered w-full" readonly value={data.fields.projectName} />
</label>
<label class="form-control w-full md:w-2/4">
<div class="label">
<span class="label-text">Project Description</span>
</div>
<input type="text" class="input input-bordered w-full" readonly value={data.fields.projectDescription} />
</label>
</div>
{#if data.fields.storeDescription}
<div class="flex flex-col gap-x-3 md:flex-row">
<label class="form-control w-full md:w-2/4">
<div class="label">
<span class="label-text">Store</span>
</div>
<input type="text" class="input input-bordered w-full" readonly value={data.fields.storeDescription} />
</label>
{#if data.fields.listingLanguageCode}
<label class="form-control w-full md:w-2/4">
<div class="label">
<span class="label-text">Store Listing Language</span>
</div>
<input type="text" class="input input-bordered w-full" readonly value={data.fields.listingLanguageCode} />
</label>
{/if}
</div>
{/if}
{#if data.fields.projectURL}
<label class="form-control w-full">
<div class="label">
<span class="label-text">App Project URL</span>
</div>
<input type="text" class="input input-bordered w-full" readonly value={data.fields.projectURL} />
</label>
{/if}
{#if data.fields.productDescription && data.fields.appType && data.fields.projectLanguageCode}
<label class="form-control w-full">
<div class="label">
<span class="label-text">Product</span>
</div>
<input type="text" class="input input-bordered w-full" readonly value={data.fields.productDescription} />
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Application Type</span>
</div>
<input type="text" class="input input-bordered w-full" readonly value={data.fields.appType} />
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Language Code</span>
</div>
<input type="text" class="input input-bordered w-full" readonly value={data.fields.projectLanguageCode} />
</label>
{/if}
</div>
{#if data.instructions}
<div class="py-2" id="instructions">
<svelte:component this={instructions[data.instructions]} />
</div>
{/if}
{#if data?.files?.length}
<div class="overflow-x-auto max-h-96">
<h3>Files</h3>
<SortTable items={data.files} />
</div>
{/if}
{#if data?.reviewers?.length}
<div class="overflow-x-auto max-h-96">
<h3>Reviewers</h3>
<SortTable items={data.reviewers} />
</div>
{/if}
</div>

<style lang=postcss>
.label-text {
font-weight: bold;
}
/*this VVV technique allows css rules to break svelte scoping downwards*/
#instructions :global(ul) {
@apply pl-10 list-disc;
}
#instructions :global(ol) {
@apply pl-10 list-decimal;
}
#instructions :global(h3) {
@apply text-info;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import { ArrowDownIcon, ArrowUpIcon } from "$lib/icons";
export let active: boolean;
export let descending: boolean;
//later: selecting one of these causes the relative sizes of the table headers to change, which looks really janky. I've tried fixing this, but have been unsuccessful. For now, at least it functions correctly, even if it doesn't look quite right.
</script>

<div class="form-control">
<label>
<span class="{active? "":"hidden"}">
{#if descending}
<ArrowDownIcon />
{:else}
<ArrowUpIcon />
{/if}
</span>
<span class="label-text"><slot></slot></span>
</label>
</div>

<style lang=postcss>
label {
@apply select-none cursor-pointer;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script lang="ts">
import SortDirectionPicker from "./SortDirectionPicker.svelte";
export let items: {[key: string]: any}[];
let current = ""; //current field being sorted
let descending = false;
const handleClick = (key: string) => {
if (current !== key) {
// change current field
descending = false;
current = key;
}
else {
if (descending) {
// reset to sort default field
handleClick("");
return; //don't sort twice
}
else {
descending = true;
}
}
// sort based on current field
// if blank, sort first field
const field = current || Object.keys(items[0])[0];
items = (typeof items[0][field] === 'string')?
// sort strings
items.sort((a, b) => {
return descending ?
b[field].localeCompare(a[field]):
a[field].localeCompare(b[field]);
}):
// sort non-strings (i.e. numbers)
items.sort((a, b) => {
return descending?
b[field] - a[field]:
a[field] - b[field];
});
}
</script>

<table class="table">
<thead>
<tr>
{#each Object.keys(items[0]) as key}
<th on:click={() => handleClick(key)}>
<SortDirectionPicker active={current === key} bind:descending={descending}>
{key}
</SortDirectionPicker>
</th>
{/each}
</tr>
</thead>
<tbody>
{#each items as item}
<tr>
{#each Object.values(item) as val}
<td>{val}</td>
{/each}
</tr>
{/each}
</tbody>
</table>
Loading

0 comments on commit 2307fae

Please sign in to comment.