Skip to content

Commit

Permalink
Merge pull request #45 from Longridge-High-School/feat-unique-asset-f…
Browse files Browse the repository at this point in the history
…ields

feat: unique asset fields, closes #41
  • Loading branch information
Arcath authored Oct 13, 2024
2 parents 7448abd + 1058880 commit 1803ec6
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 12 deletions.
74 changes: 69 additions & 5 deletions app/routes/app.$assetslug.$entry.edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import {
redirect,
unstable_parseMultipartFormData
} from '@remix-run/node'
import {asyncForEach, indexedBy, invariant} from '@arcath/utils'
import {useLoaderData, useActionData} from '@remix-run/react'
import {asyncMap, indexedBy, invariant} from '@arcath/utils'
import {getUniqueCountForAssetField} from '@prisma/client/sql'

import {ensureUser} from '~/lib/utils/ensure-user'
import {getPrisma} from '~/lib/prisma.server'
import {useLoaderData} from '@remix-run/react'
import {FIELDS} from '~/lib/fields/field'
import {Button} from '~/lib/components/button'
import {pageTitle} from '~/lib/utils/page-title'
Expand Down Expand Up @@ -80,9 +81,14 @@ export const action = async ({request, params}: ActionFunctionArgs) => {
data: {aclId: acl}
})

await asyncForEach(
const results = await asyncMap(
asset.assetFields,
async ({fieldId, field, id}): Promise<void> => {
async ({
fieldId,
field,
id,
unique
}): Promise<{error: string; field: string} | boolean> => {
const entryValue = await prisma.value.findFirst({
where: {entryId: params.entry!, fieldId}
})
Expand All @@ -93,6 +99,36 @@ export const action = async ({request, params}: ActionFunctionArgs) => {
entryValue ? entryValue.value : ''
)

switch (unique) {
case 1: {
const [withinAssetCount] = await prisma.$queryRawTyped(
getUniqueCountForAssetField(params.entry!, fieldId, value)
)
if (withinAssetCount['COUNT(*)'] > 0) {
return {
error: `Value is not unique across all ${asset.name}`,
field: fieldId
}
}
break
}
case 2: {
const withinFieldCount = await prisma.value.count({
where: {fieldId, value}
})
if (withinFieldCount > 0) {
return {
error: 'Value is not unique across all of the documentation',
field: fieldId
}
}
break
}
case 0:
default:
break
}

if (entryValue) {
if (value !== entryValue.value) {
await prisma.valueHistory.create({
Expand All @@ -110,15 +146,23 @@ export const action = async ({request, params}: ActionFunctionArgs) => {
data: {value}
})

return
return true
}

await prisma.value.create({
data: {entryId: params.entry!, fieldId, value, lastEditedById: user.id}
})

return true
}
)

const flags = results.filter(v => v !== true)

if (flags.length > 0) {
return json({errors: flags})
}

return redirect(`/app/${params.assetslug}/${params.entry}`)
}

Expand All @@ -128,14 +172,34 @@ export const meta: MetaFunction<typeof loader> = ({data}) => {

const Asset = () => {
const {entry, acls} = useLoaderData<typeof loader>()
const actionData = useActionData<typeof action>()

const {asset} = entry

const fieldValues = indexedBy('fieldId', entry.values)
const fields = indexedBy('fieldId', asset.assetFields)

return (
<div className="entry">
<h2>Edit {asset.singular}</h2>
{actionData && actionData.errors ? (
<div className="bg-red-200 border-red-300 border p-2 mb-8">
<h3 className="text-lg">Save Errors</h3>
<ul>
{actionData.errors
.filter(v => v !== false)
.map(({field, error}) => {
return (
<li key={field}>
{fields[field].field.name}: {error}
</li>
)
})}
</ul>
</div>
) : (
''
)}
<form method="POST" encType="multipart/form-data">
{asset.assetFields.map(({id, helperText, field}) => {
const FieldComponent = (params: {
Expand Down
78 changes: 74 additions & 4 deletions app/routes/app.$assetslug.add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {
redirect,
unstable_parseMultipartFormData
} from '@remix-run/node'
import {useLoaderData} from '@remix-run/react'
import {asyncForEach} from '@arcath/utils'
import {useLoaderData, useActionData} from '@remix-run/react'
import {asyncMap, indexedBy} from '@arcath/utils'
import {getUniqueCountForAssetField} from '@prisma/client/sql'

import {ensureUser} from '~/lib/utils/ensure-user'
import {getPrisma} from '~/lib/prisma.server'
Expand Down Expand Up @@ -52,21 +53,68 @@ export const action = async ({request, params}: ActionFunctionArgs) => {
data: {assetId: asset.id, aclId: asset.aclId}
})

await asyncForEach(
const results = await asyncMap(
asset.assetFields,
async ({fieldId, field, id}): Promise<void> => {
async ({
fieldId,
field,
id,
unique
}): Promise<{error: string; field: string} | boolean> => {
const value = await FIELD_HANDLERS[field.type].valueSetter(
formData,
id,
''
)

switch (unique) {
case 1: {
const [withinAssetCount] = await prisma.$queryRawTyped(
getUniqueCountForAssetField(params.entry!, fieldId, value)
)
if (withinAssetCount['COUNT(*)'] > 0) {
return {
error: `Value is not unique across all ${asset.name}`,
field: fieldId
}
}
break
}
case 2: {
const withinFieldCount = await prisma.value.count({
where: {fieldId, value}
})
if (withinFieldCount > 0) {
return {
error: 'Value is not unique across all of the documentation',
field: fieldId
}
}
break
}
case 0:
default:
break
}

await prisma.value.create({
data: {entryId: entry.id, fieldId, value, lastEditedById: user.id}
})

return true
}
)

// If validation fails, need to delete the new entry.
const flags = results.filter(v => v !== true)

if (flags.length > 0) {
await prisma.value.deleteMany({where: {entryId: entry.id}})
await prisma.entry.delete({where: {id: entry.id}})

return json({errors: flags})
}

return redirect(`/app/${params.assetslug}/${entry.id}`)
}

Expand All @@ -81,9 +129,31 @@ export const meta: MetaFunction<typeof loader> = ({data}) => {
const Asset = () => {
const {asset} = useLoaderData<typeof loader>()

const actionData = useActionData<typeof action>()

const fields = indexedBy('fieldId', asset.assetFields)

return (
<div className="entry">
<h2>Add {asset.singular}</h2>
{actionData && actionData.errors ? (
<div className="bg-red-200 border-red-300 border p-2 mb-8">
<h3 className="text-lg">Save Errors</h3>
<ul>
{actionData.errors
.filter(v => v !== false)
.map(({field, error}) => {
return (
<li key={field}>
{fields[field].field.name}: {error}
</li>
)
})}
</ul>
</div>
) : (
''
)}
<form method="POST" encType="multipart/form-data">
{asset.assetFields.map(({id, helperText, field}) => {
const FieldComponent = (params: {
Expand Down
37 changes: 34 additions & 3 deletions app/routes/app.asset-manager.$asset.$assetfield.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import {invariant} from '@arcath/utils'
import {ensureUser} from '~/lib/utils/ensure-user'
import {getPrisma} from '~/lib/prisma.server'
import {Button} from '~/lib/components/button'
import {Label, Input, HelperText, Checkbox} from '~/lib/components/input'
import {
Label,
Input,
HelperText,
Checkbox,
Select
} from '~/lib/components/input'
import {pageTitle} from '~/lib/utils/page-title'

export const loader = async ({request, params}: LoaderFunctionArgs) => {
Expand Down Expand Up @@ -39,20 +45,30 @@ export const action = async ({request, params}: ActionFunctionArgs) => {
const order = formData.get('order') as string | undefined
const displayOnTable = formData.get('displayontable') as null | 'on'
const hidden = formData.get('hidden') as null | 'on'
const unique = formData.get('unique') as string | undefined

invariant(helper)
invariant(order)
invariant(unique)

await prisma.assetField.update({
const updatedAssetField = await prisma.assetField.update({
where: {id: params.assetfield},
data: {
helperText: helper,
order: parseInt(order),
displayOnTable: displayOnTable === 'on',
hidden: hidden === 'on'
hidden: hidden === 'on',
unique: parseInt(unique)
}
})

if (parseInt(unique) === 2) {
await prisma.assetField.updateMany({
where: {fieldId: updatedAssetField.fieldId},
data: {unique: 2}
})
}

return redirect(`/app/asset-manager/${params.asset}`)
}

Expand Down Expand Up @@ -109,6 +125,21 @@ const AssetManagerAddFieldToAsset = () => {
Hide this field from the display (not revisions) and forms.
</HelperText>
</Label>
<Label>
Unique
<Select name="unique" defaultValue={assetField.unique}>
<option value="0">Not Unique</option>
<option value="1">Unique within this Asset</option>
<option value="2">
Unique within all assets using this field.
</option>
</Select>
<HelperText>
Control the uniqueness of this field. If you choose &quot;Unique
within all assets using this field&quot; all other assets will be
updated to the same setting.
</HelperText>
</Label>
<Button className="bg-success">Update Field</Button>
</form>
</div>
Expand Down
7 changes: 7 additions & 0 deletions docs/docs/concepts/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ another asset.

Assets have an icon which is any emoji. Be aware that emoji does render
differently on each OS, even within versions.

## Fields

Any field can be added to an asset. When adding a field you set the _Helper
Text_ which is the text that will appear under the field in the editors. The
order is a numerical order for the field in the forms and display. A field can
be set to unique within the asset, or globally within the whole of Net-Doc.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_AssetField" (
"id" TEXT NOT NULL PRIMARY KEY,
"order" INTEGER NOT NULL,
"helperText" TEXT NOT NULL DEFAULT '',
"displayOnTable" BOOLEAN NOT NULL DEFAULT false,
"hidden" BOOLEAN NOT NULL DEFAULT false,
"unique" INTEGER NOT NULL DEFAULT 0,
"assetId" TEXT NOT NULL,
"fieldId" TEXT NOT NULL,
CONSTRAINT "AssetField_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "AssetField_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "Field" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_AssetField" ("assetId", "displayOnTable", "fieldId", "helperText", "hidden", "id", "order") SELECT "assetId", "displayOnTable", "fieldId", "helperText", "hidden", "id", "order" FROM "AssetField";
DROP TABLE "AssetField";
ALTER TABLE "new_AssetField" RENAME TO "AssetField";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ model AssetField {
helperText String @default("")
displayOnTable Boolean @default(false)
hidden Boolean @default(false)
unique Int @default(0) // Unique Modes: 0 - Not, 1 - Within Asset, 2 - Unique within Field
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
assetId String
Expand Down
13 changes: 13 additions & 0 deletions prisma/sql/getUniqueCountForAssetField.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- @param {String} $1:entryId The ID of the entry being checked
-- @param {String} $2:fieldId The ID of the field being checked
-- @param {String} $3:value The value to be checked
SELECT
COUNT(*)
FROM
Value
WHERE
Value.entryId IN (SELECT Entry.id FROM Entry WHERE Entry.assetId = (SELECT Asset.id FROM Asset WHERE Asset.id = (SELECT Entry.assetId FROM Entry WHERE Entry.id = $1)))
AND
Value.fieldId = $2
AND
Value.value = $3

0 comments on commit 1803ec6

Please sign in to comment.