diff --git a/app/routes/app.$assetslug.$entry.edit.tsx b/app/routes/app.$assetslug.$entry.edit.tsx index 0518807..0055a9a 100644 --- a/app/routes/app.$assetslug.$entry.edit.tsx +++ b/app/routes/app.$assetslug.$entry.edit.tsx @@ -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' @@ -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 => { + async ({ + fieldId, + field, + id, + unique + }): Promise<{error: string; field: string} | boolean> => { const entryValue = await prisma.value.findFirst({ where: {entryId: params.entry!, fieldId} }) @@ -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({ @@ -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}`) } @@ -128,14 +172,34 @@ export const meta: MetaFunction = ({data}) => { const Asset = () => { const {entry, acls} = useLoaderData() + const actionData = useActionData() const {asset} = entry const fieldValues = indexedBy('fieldId', entry.values) + const fields = indexedBy('fieldId', asset.assetFields) return (

Edit {asset.singular}

+ {actionData && actionData.errors ? ( +
+

Save Errors

+
    + {actionData.errors + .filter(v => v !== false) + .map(({field, error}) => { + return ( +
  • + {fields[field].field.name}: {error} +
  • + ) + })} +
+
+ ) : ( + '' + )}
{asset.assetFields.map(({id, helperText, field}) => { const FieldComponent = (params: { diff --git a/app/routes/app.$assetslug.add.tsx b/app/routes/app.$assetslug.add.tsx index dac100a..28c4dac 100644 --- a/app/routes/app.$assetslug.add.tsx +++ b/app/routes/app.$assetslug.add.tsx @@ -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' @@ -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 => { + 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}`) } @@ -81,9 +129,31 @@ export const meta: MetaFunction = ({data}) => { const Asset = () => { const {asset} = useLoaderData() + const actionData = useActionData() + + const fields = indexedBy('fieldId', asset.assetFields) + return (

Add {asset.singular}

+ {actionData && actionData.errors ? ( +
+

Save Errors

+
    + {actionData.errors + .filter(v => v !== false) + .map(({field, error}) => { + return ( +
  • + {fields[field].field.name}: {error} +
  • + ) + })} +
+
+ ) : ( + '' + )} {asset.assetFields.map(({id, helperText, field}) => { const FieldComponent = (params: { diff --git a/app/routes/app.asset-manager.$asset.$assetfield.tsx b/app/routes/app.asset-manager.$asset.$assetfield.tsx index 62bc2f7..5822373 100644 --- a/app/routes/app.asset-manager.$asset.$assetfield.tsx +++ b/app/routes/app.asset-manager.$asset.$assetfield.tsx @@ -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) => { @@ -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}`) } @@ -109,6 +125,21 @@ const AssetManagerAddFieldToAsset = () => { Hide this field from the display (not revisions) and forms. +
diff --git a/docs/docs/concepts/assets.md b/docs/docs/concepts/assets.md index 60b2866..e8521d2 100644 --- a/docs/docs/concepts/assets.md +++ b/docs/docs/concepts/assets.md @@ -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. diff --git a/prisma/migrations/20241009135947_add_unique_to_asset_field/migration.sql b/prisma/migrations/20241009135947_add_unique_to_asset_field/migration.sql new file mode 100644 index 0000000..1ba9a24 --- /dev/null +++ b/prisma/migrations/20241009135947_add_unique_to_asset_field/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bbfab9b..267af33 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 diff --git a/prisma/sql/getUniqueCountForAssetField.sql b/prisma/sql/getUniqueCountForAssetField.sql new file mode 100644 index 0000000..a30971c --- /dev/null +++ b/prisma/sql/getUniqueCountForAssetField.sql @@ -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 \ No newline at end of file