Skip to content

Commit

Permalink
Merge pull request #930 from rockingrohit9639/main
Browse files Browse the repository at this point in the history
Assign custom fields to categories
  • Loading branch information
DonKoko authored Apr 30, 2024
2 parents 80c9573 + b32f69e commit 2bb686f
Show file tree
Hide file tree
Showing 17 changed files with 345 additions and 55 deletions.
1 change: 1 addition & 0 deletions app/components/assets/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export const AssetForm = ({
initialDataKey="categories"
countKey="totalCategories"
closeOnSelect
selectionMode="set"
extraContent={
<Button
to="/categories/new"
Expand Down
49 changes: 48 additions & 1 deletion app/components/custom-fields/form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from "react";
import type { CustomField } from "@prisma/client";
import { CustomFieldType } from "@prisma/client";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { Form, Link, useActionData, useNavigation } from "@remix-run/react";
import { useAtom } from "jotai";
import { useZorm } from "react-zorm";
import { z } from "zod";
Expand All @@ -13,6 +13,7 @@ import { FIELD_TYPE_NAME } from "~/utils/custom-fields";
import { isFormProcessing } from "~/utils/form";
import { getValidationErrors } from "~/utils/http";
import { zodFieldIsRequired } from "~/utils/zod";
import CategoriesInput from "../forms/categories-input";
import FormRow from "../forms/form-row";
import Input from "../forms/input";
import OptionBuilder from "../forms/option-builder";
Expand Down Expand Up @@ -45,6 +46,7 @@ export const NewCustomFieldFormSchema = z.object({
.transform((val) => (val === "on" ? true : false)),
organizationId: z.string(),
options: z.array(z.string()).optional(),
categories: z.array(z.string()).optional(),
});

/** Pass props of the values to be used as default for the form fields */
Expand All @@ -56,6 +58,7 @@ interface Props {
active?: CustomField["active"];
options?: CustomField["options"];
isEdit?: boolean;
categories?: string[];
}

const FIELD_TYPE_DESCRIPTION: { [key in CustomFieldType]: string } = {
Expand All @@ -75,6 +78,7 @@ export const CustomFieldForm = ({
type,
active,
isEdit = false,
categories = [],
}: Props) => {
const navigation = useNavigation();
const zo = useZorm("NewQuestionWizardScreen", NewCustomFieldFormSchema);
Expand All @@ -84,6 +88,7 @@ export const CustomFieldForm = ({
const [selectedType, setSelectedType] = useState<CustomFieldType>(
type || "TEXT"
);
const [useCategories, setUseCategories] = useState(categories.length > 0);

const [, updateTitle] = useAtom(updateDynamicTitleAtom);

Expand Down Expand Up @@ -220,6 +225,48 @@ export const CustomFieldForm = ({
) : null}
</FormRow>

<div>
<FormRow
rowLabel="Category"
subHeading={
<p>
Select asset categories for which you want to use this custom
field.{" "}
<Link
to="https://www.shelf.nu/knowledge-base/linking-custom-fields-to-categories"
target="_blank"
>
Read more
</Link>
</p>
}
>
<div className="mb-3 flex gap-3">
<Switch
disabled={disabled}
checked={useCategories}
onCheckedChange={setUseCategories}
/>
<div>
<label className="text-base font-medium text-gray-700">
Use for select categories
</label>
<p className="text-[14px] text-gray-600">
In case you only want to use this custom field for asset with
certain categories.
</p>
</div>
</div>

{useCategories && (
<CategoriesInput
categories={categories}
name={(i) => zo.fields.categories(i)()}
/>
)}
</FormRow>
</div>

<div>
<FormRow
rowLabel="Help Text"
Expand Down
18 changes: 15 additions & 3 deletions app/components/dynamic-select/dynamic-select.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useRef, useState } from "react";
import React, { useMemo, useRef, useState } from "react";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import {
Popover,
Expand Down Expand Up @@ -36,6 +36,8 @@ type Props = ModelFilterProps & {
placeholder?: string;
closeOnSelect?: boolean;
valueExtractor?: (item: ModelFilterItem) => string;
excludeItems?: string[];
onChange?: ((value: string) => void) | null;
};

export default function DynamicSelect({
Expand All @@ -55,6 +57,9 @@ export default function DynamicSelect({
placeholder = `Select ${model.name}`,
closeOnSelect = false,
valueExtractor,
selectionMode = "none",
excludeItems,
onChange = null,
}: Props) {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const triggerRef = useRef<HTMLDivElement>(null);
Expand All @@ -79,10 +84,16 @@ export default function DynamicSelect({
model,
countKey,
initialDataKey,
selectionMode: "none",
selectionMode,
valueExtractor,
});

const itemsToRender = useMemo(
() =>
excludeItems ? items.filter((i) => !excludeItems.includes(i.id)) : items,
[excludeItems, items]
);

return (
<>
<div className="relative w-full">
Expand Down Expand Up @@ -170,7 +181,7 @@ export default function DynamicSelect({
modelName={model.name}
/>
)}
{items.map((item) => (
{itemsToRender.map((item) => (
<div
key={item.id}
className={tw(
Expand All @@ -180,6 +191,7 @@ export default function DynamicSelect({
onClick={() => {
setSelectedValue(item.id);
handleSelectItemChange(item.id);
onChange && onChange(item.id);
if (closeOnSelect) {
setIsPopoverOpen(false);
}
Expand Down
72 changes: 72 additions & 0 deletions app/components/forms/categories-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useState } from "react";
import { tw } from "~/utils/tw";
import DynamicSelect from "../dynamic-select/dynamic-select";
import { Button } from "../shared/button";

type CategoriesInputProps = {
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
name: (index: number) => string;
categories: string[];
};

export default function CategoriesInput({
className,
style,
disabled,
name,
categories: incomingCategories,
}: CategoriesInputProps) {
const [categories, setCategories] = useState<string[]>(
incomingCategories.length === 0 ? [""] : incomingCategories
);

return (
<div className={tw("w-full", className)} style={style}>
{categories.map((category, i) => (
<div key={i} className="mb-3 flex items-center gap-x-2">
<DynamicSelect
disabled={disabled}
fieldName={name(i)}
defaultValue={category}
model={{ name: "category", queryKey: "name" }}
label="Category"
initialDataKey="categories"
countKey="totalCategories"
placeholder="Select Category"
className="flex-1"
excludeItems={categories}
onChange={(value) => {
categories[i] = value;
setCategories([...categories]);
}}
/>

<Button
icon="x"
className="py-2"
variant="outline"
type="button"
onClick={() => {
categories.splice(i, 1);
setCategories([...categories]);
}}
/>
</div>
))}

<Button
icon="plus"
className="py-3"
variant="link"
type="button"
onClick={() => {
setCategories((prev) => [...prev, ""]);
}}
>
Add another category
</Button>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "_CategoryToCustomField" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "_CategoryToCustomField_AB_unique" ON "_CategoryToCustomField"("A", "B");

-- CreateIndex
CREATE INDEX "_CategoryToCustomField_B_index" ON "_CategoryToCustomField"("B");

-- AddForeignKey
ALTER TABLE "_CategoryToCustomField" ADD CONSTRAINT "_CategoryToCustomField_A_fkey" FOREIGN KEY ("A") REFERENCES "Category"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_CategoryToCustomField" ADD CONSTRAINT "_CategoryToCustomField_B_fkey" FOREIGN KEY ("B") REFERENCES "CustomField"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- This is an empty migration.

-- Enable RLS
ALTER TABLE "_CategoryToCustomField" ENABLE row level security;
24 changes: 14 additions & 10 deletions app/database/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}

Expand Down Expand Up @@ -137,6 +137,8 @@ model Category {
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
userId String
customFields CustomField[]
organization Organization @relation(fields: [organizationId], references: [id], onUpdate: Cascade)
organizationId String
Expand All @@ -160,7 +162,7 @@ model Tag {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//@@unique([lower(name), organizationId]) //prisma doesnt support case insensitive unique index yet
//@@unique([lower(name), organizationId]) //prisma doesnt support case insensitive unique index yet
}

model Note {
Expand Down Expand Up @@ -303,7 +305,7 @@ model Location {
assets Asset[]
// @@unique([lower(name), organizationId]) //prisma doesnt support case insensitive unique index yet
// @@unique([lower(name), organizationId]) //prisma doesnt support case insensitive unique index yet
}

// Master data for roles
Expand All @@ -326,17 +328,17 @@ model TeamMember {
id String @id @unique @default(cuid())
name String
organization Organization @relation(fields: [organizationId], references: [id], onUpdate: Cascade)
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onUpdate: Cascade)
organizationId String
custodies Custody[]
receivedInvites Invite[]
user User? @relation(fields: [userId], references: [id], onUpdate: Cascade)
user User? @relation(fields: [userId], references: [id], onUpdate: Cascade)
userId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
bookings Booking[]
bookings Booking[]
}

model Custody {
Expand Down Expand Up @@ -459,6 +461,8 @@ model CustomField {
createdBy User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
userId String
categories Category[]
// Datetime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Expand Down Expand Up @@ -580,13 +584,13 @@ model Booking {
name String
status BookingStatus @default(DRAFT)
activeSchedulerReference String?
activeSchedulerReference String?
// Relationships
creator User @relation("creator", fields: [creatorId], references: [id], onDelete: Cascade, onUpdate: Cascade)
creatorId String
custodianUser User? @relation("custodian", fields: [custodianUserId], references: [id], onDelete: Cascade, onUpdate: Cascade)
custodianUser User? @relation("custodian", fields: [custodianUserId], references: [id], onDelete: Cascade, onUpdate: Cascade)
custodianUserId String?
custodianTeamMember TeamMember? @relation(fields: [custodianTeamMemberId], references: [id], onDelete: Cascade, onUpdate: Cascade)
Expand Down
1 change: 0 additions & 1 deletion app/hooks/use-model-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export function useModelFilters({
if (searchQuery && fetcher.data && !fetcher.data.error) {
return itemsWithExtractedValue(fetcher.data.filters, valueExtractor);
}

return itemsWithExtractedValue(initialData[initialDataKey], valueExtractor);
}, [fetcher.data, initialData, initialDataKey, searchQuery, valueExtractor]);

Expand Down
Loading

0 comments on commit 2bb686f

Please sign in to comment.