Skip to content

Commit

Permalink
show where inputs are used
Browse files Browse the repository at this point in the history
  • Loading branch information
xvvvyz committed Aug 30, 2024
1 parent 83a9df6 commit 1b7f622
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 51 deletions.
4 changes: 2 additions & 2 deletions app/(pages)/(with-nav)/inputs/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Empty from '@/_components/empty';
import FilterableInputs from '@/_components/filterable-inputs';
import listInputs from '@/_queries/list-inputs';
import listInputsWithUses from '@/_queries/list-inputs-with-uses';
import InformationCircleIcon from '@heroicons/react/24/outline/InformationCircleIcon';
import { sortBy } from 'lodash';

const Page = async () => {
const { data: inputs } = await listInputs();
const { data: inputs } = await listInputsWithUses();

if (!inputs?.length) {
return (
Expand Down
47 changes: 44 additions & 3 deletions app/(pages)/@modal/(layout)/inputs/[inputId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import Avatar from '@/_components/avatar';
import Button from '@/_components/button';
import InputForm from '@/_components/input-form';
import * as Modal from '@/_components/modal';
import PageModalHeader from '@/_components/page-modal-header';
import getInput from '@/_queries/get-input';
import listSubjectsByTeamId from '@/_queries/list-subjects-by-team-id';

import getInputWithUses, {
GetInputWithUsesData,
} from '@/_queries/get-input-with-uses';

interface PageProps {
params: {
inputId: string;
Expand All @@ -12,15 +17,51 @@ interface PageProps {

const Page = async ({ params: { inputId } }: PageProps) => {
const [{ data: input }, { data: subjects }] = await Promise.all([
getInput(inputId),
getInputWithUses(inputId),
listSubjectsByTeamId(),
]);

if (!input || !subjects) return null;

const usedBy =
input?.uses?.reduce<
Map<string, NonNullable<GetInputWithUsesData>['uses'][0]['subject']>
>((acc, eventType) => {
if (!eventType.subject) return acc;
acc.set(eventType.subject.id, eventType.subject);
return acc;
}, new Map()) ?? new Map();

return (
<Modal.Content>
<PageModalHeader title={input.label} />
<PageModalHeader title="Edit input" />
{!!usedBy.size && (
<div className="pb-6">
<div className="border-y border-alpha-1 px-4 py-4 sm:px-8">
<div className="smallcaps flex flex-wrap items-center gap-3">
<div className="text-fg-4">Used by</div>
{Array.from(usedBy.values())
.filter((subject) => !!subject)
.sort((a, b) => a.name.localeCompare(b.name))
.map((subject) => (
<Button
className="m-0 mr-1 p-0"
href={`/subjects/${subject.id}`}
key={subject.id}
variant="link"
>
<Avatar
className="-my-[0.15rem] size-5"
file={subject.image_uri}
id={subject.id}
/>
{subject.name}
</Button>
))}
</div>
</div>
</div>
)}
<InputForm input={input} subjects={subjects} />
</Modal.Content>
);
Expand Down
80 changes: 44 additions & 36 deletions app/_components/filterable-inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import Avatar from '@/_components/avatar';
import Button from '@/_components/button';
import Input from '@/_components/input';
import InputMenu from '@/_components/input-menu';
import INPUT_TYPE_LABELS from '@/_constants/constant-input-type-labels';
import { ListInputsData } from '@/_queries/list-inputs';
import { ListInputsWithUsesData } from '@/_queries/list-inputs-with-uses';
import { usePrevious } from '@uidotdev/usehooks';
import Fuse from 'fuse.js';

Expand All @@ -20,21 +19,21 @@ import {
} from 'react';

interface FilterableInputsProps {
inputs: NonNullable<ListInputsData>;
inputs: NonNullable<ListInputsWithUsesData>;
}

const FilterableInputs = ({ inputs }: FilterableInputsProps) => {
const [, startTransition] = useTransition();
const ref = useRef<HTMLInputElement>(null);

const [filteredInputs, setFilteredInputs] = useState<
NonNullable<ListInputsData>
NonNullable<ListInputsWithUsesData>
>([]);

const fuse = useMemo(
() =>
new Fuse(inputs, {
keys: ['label', 'subjects.name', 'type'],
keys: ['label', 'subjects.name', 'type', 'uses.subject.name'],
threshold: 0.3,
}),
[inputs],
Expand Down Expand Up @@ -71,39 +70,48 @@ const FilterableInputs = ({ inputs }: FilterableInputsProps) => {
/>
</div>
<ul className="mx-4 overflow-hidden rounded border border-alpha-1 bg-bg-2 py-1 empty:hidden">
{filteredInputs.map((input) => (
<li
className="flex items-stretch transition-colors hover:bg-alpha-1"
key={input.id}
>
<Button
className="m-0 w-full min-w-0 gap-6 px-4 py-3 pr-0 leading-snug"
href={`/inputs/${input.id}`}
scroll={false}
variant="link"
{filteredInputs.map((input) => {
const usedBy = input.uses.reduce(
(acc, eventType) => acc.add(eventType.subject?.id),
new Set(),
);

return (
<li
className="flex items-stretch transition-colors hover:bg-alpha-1"
key={input.id}
>
<div className="min-w-0">
<div className="truncate">{input.label}</div>
<div className="smallcaps pb-0.5 pt-1 text-fg-4">
{INPUT_TYPE_LABELS[input.type]}
</div>
</div>
{!!input.subjects.length && (
<div className="-my-0.5 ml-auto flex shrink-0 gap-1.5">
{input.subjects.map(({ id, image_uri }) => (
<Avatar
className="size-5"
file={image_uri}
key={id}
id={id}
/>
))}
<Button
className="m-0 w-full min-w-0 gap-6 px-4 py-3 pr-0 leading-snug"
href={`/inputs/${input.id}`}
scroll={false}
variant="link"
>
<div className="min-w-0">
<div className="truncate">{input.label}</div>
<div className="smallcaps pb-0.5 pt-1 text-fg-4">
{usedBy.size
? `Used by ${usedBy.size} subject${usedBy.size === 1 ? '' : 's'}`
: 'Not used'}
</div>
</div>
)}
</Button>
<InputMenu inputId={input.id} />
</li>
))}
{!!input.subjects.length && (
<div className="-my-0.5 ml-auto flex shrink-0 gap-1.5">
{input.subjects.map(({ id, image_uri }) => (
<Avatar
className="size-5"
file={image_uri}
key={id}
id={id}
/>
))}
</div>
)}
</Button>
<InputMenu inputId={input.id} />
</li>
);
})}
</ul>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions app/_components/input-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ const InputForm = ({
placeholder="All subjects…"
tooltip={
<>
If this input isn&rsquo;t applicable to all of your subjects,
you can specify the relevant subjects here.
The input will only be available for the&nbsp;specified
subjects.
</>
}
value={field.value as PropsValue<IOption>}
Expand Down
6 changes: 3 additions & 3 deletions app/_components/page-modal-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ const PageModalHeader = ({
}: PageModalHeaderProps) => (
<div
className={twMerge(
'flex items-start justify-between gap-8 px-4 pb-6 pt-8 sm:px-8',
'flex items-start justify-between gap-8 px-4 py-6 sm:px-8',
className,
)}
>
<div className="min-w-0">
{title && (
<Modal.Title className="-my-1 truncate text-xl">{title}</Modal.Title>
<Modal.Title className="-mb-1 truncate text-xl">{title}</Modal.Title>
)}
{subtitle}
</div>
<div className="relative -top-1 right-1 flex shrink-0 items-center gap-6">
<div className="relative right-1 top-0 flex shrink-0 items-center gap-6">
{right}
<PageModalBackIconButton
icon={<XMarkIcon className="w-7" />}
Expand Down
4 changes: 2 additions & 2 deletions app/_components/page-modal-loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ interface LoadingProps {

const PageModalLoading = ({ className }: LoadingProps) => (
<Modal.Content className={className}>
<div className="align-start flex justify-between px-4 py-8 sm:px-8">
<div className="align-start flex justify-between px-4 py-6 sm:px-8">
<Modal.Title className="h-6 w-32 animate-pulse rounded-sm bg-alpha-3" />
<PageModalBackIconButton
className="relative -top-1 right-1 shrink-0"
className="relative right-1 top-0 shrink-0"
icon={<XMarkIcon className="w-7" />}
/>
</div>
Expand Down
27 changes: 27 additions & 0 deletions app/_queries/get-input-with-uses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';

const getInputWithUses = (inputId: string) =>
createServerSupabaseClient()
.from('inputs')
.select(
`
id,
label,
options:input_options(id, label),
settings,
subjects(id),
type,
uses:event_types(subject:subjects(id, name, image_uri))`,
)
.eq('id', inputId)
.eq('subjects.deleted', false)
.not('subjects.archived', 'is', null)
.eq('event_types.subjects.deleted', false)
.order('order', { referencedTable: 'options' })
.single();

export type GetInputWithUsesData = Awaited<
ReturnType<typeof getInputWithUses>
>['data'];

export default getInputWithUses;
1 change: 1 addition & 0 deletions app/_queries/get-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const getInput = (inputId: string) =>
)
.eq('id', inputId)
.eq('subjects.deleted', false)
.not('subjects.archived', 'is', null)
.order('order', { referencedTable: 'options' })
.single();

Expand Down
2 changes: 1 addition & 1 deletion app/_queries/list-inputs-by-subject-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const listInputsBySubjectId = async (subjectId: string) => {
.eq('archived', false)
.not('id', 'in', `(${blacklist.data.map((is) => is.input_id).join(',')})`)
.eq('subjects.deleted', false)
.eq('subjects.archived', false)
.not('subjects.archived', 'is', null)
.order('name', { referencedTable: 'subjects' })
.order('label');
};
Expand Down
27 changes: 27 additions & 0 deletions app/_queries/list-inputs-with-uses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import getCurrentUser from '@/_queries/get-current-user';
import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';

const listInputsWithUses = async () =>
createServerSupabaseClient()
.from('inputs')
.select(
`
id,
label,
subjects(id, image_uri, name),
type,
uses:event_types(subject:subjects(id, name))`,
)
.eq('team_id', (await getCurrentUser())?.id ?? '')
.eq('archived', false)
.eq('subjects.deleted', false)
.not('subjects.archived', 'is', null)
.eq('event_types.subjects.deleted', false)
.order('name', { referencedTable: 'subjects' })
.order('label');

export type ListInputsWithUsesData = Awaited<
ReturnType<typeof listInputsWithUses>
>['data'];

export default listInputsWithUses;
4 changes: 2 additions & 2 deletions app/_queries/list-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import createServerSupabaseClient from '@/_utilities/create-server-supabase-clie
const listInputs = async () =>
createServerSupabaseClient()
.from('inputs')
.select('id, label, subjects(id, image_uri, name), type')
.select('id, label, subjects(id, image_uri, name)')
.eq('team_id', (await getCurrentUser())?.id ?? '')
.eq('archived', false)
.eq('subjects.deleted', false)
.eq('subjects.archived', false)
.not('subjects.archived', 'is', null)
.order('name', { referencedTable: 'subjects' })
.order('label');

Expand Down

0 comments on commit 1b7f622

Please sign in to comment.