Generate default values if a view has filters for records

This commit is contained in:
Hakan Shehu
2024-11-20 11:46:44 +01:00
parent 6bb7077f0e
commit e596d6494a
11 changed files with 403 additions and 51 deletions

View File

@@ -19,7 +19,7 @@ export class RecordCreateMutationHandler
parentId: input.databaseId,
databaseId: input.databaseId,
name: input.name ?? '',
fields: {},
fields: input.fields ?? {},
content: {},
};

View File

@@ -7,40 +7,37 @@ interface CalendarViewDayProps {
date: Date;
month: Date;
records: RecordNode[];
onCreate?: () => void;
}
export const CalendarViewDay = ({
date,
month,
records,
onCreate,
}: CalendarViewDayProps) => {
const isToday = isSameDay(date, new Date());
const canCreateRecord = false;
const dateMonth = date.getMonth();
const displayMonth = month.getMonth();
const isOutside = dateMonth !== displayMonth;
return (
<div className="animate-fade-in group flex h-full w-full flex-col gap-1">
<div className="animate-fade-in group/calendar-day flex h-full w-full flex-col gap-1">
<div
className={cn(
'flex justify-between text-sm',
'flex w-full justify-end text-sm',
isOutside ? 'text-muted-foreground' : ''
)}
>
<Plus
className={cn(
'size-4 cursor-pointer opacity-0',
canCreateRecord ? 'group-hover:opacity-100' : ''
)}
onClick={() => {}}
/>
<p
className={
isToday ? 'rounder-md rounded bg-red-500 p-0.5 text-white' : ''
}
>
{onCreate && (
<div className="flex-grow">
<Plus
className="size-4 cursor-pointer opacity-0 group-hover/calendar-day:opacity-100"
onClick={onCreate}
/>
</div>
)}
<p className={isToday ? 'rounded-md bg-red-500 p-0.5 text-white' : ''}>
{date.getDate()}
</p>
</div>

View File

@@ -1,6 +1,11 @@
import React from 'react';
import { buttonVariants } from '@/renderer/components/ui/button';
import { cn, getDisplayedDates, toUTCDate } from '@/shared/lib/utils';
import {
cn,
getDisplayedDates,
isSameDay,
toUTCDate,
} from '@/shared/lib/utils';
import { DayPicker, DayProps } from 'react-day-picker';
import { CalendarViewDay } from '@/renderer/components/databases/calendars/calendar-view-day';
import { FieldAttributes, ViewFilterAttributes } from '@colanode/core';
@@ -9,6 +14,7 @@ import { filterRecords } from '@/shared/lib/databases';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { useView } from '@/renderer/contexts/view';
import { useDatabase } from '@/renderer/contexts/database';
interface CalendarViewGridProps {
field: FieldAttributes;
@@ -16,6 +22,7 @@ interface CalendarViewGridProps {
export const CalendarViewGrid = ({ field }: CalendarViewGridProps) => {
const workspace = useWorkspace();
const database = useDatabase();
const view = useView();
const [month, setMonth] = React.useState(new Date());
@@ -39,7 +46,7 @@ export const CalendarViewGrid = ({ field }: CalendarViewGridProps) => {
},
];
const { records } = useRecordsQuery(filters, view.sorts);
const { records } = useRecordsQuery(filters, view.sorts, 200);
return (
<DayPicker
@@ -96,11 +103,21 @@ export const CalendarViewGrid = ({ field }: CalendarViewGridProps) => {
workspace.userId
);
const canCreate =
(field.type === 'createdAt' && isSameDay(props.date, new Date())) ||
field.type === 'date';
const onCreate =
database.canCreateRecord && canCreate
? () => view.createRecord([filter])
: undefined;
return (
<CalendarViewDay
date={toUTCDate(props.date)}
month={props.displayMonth}
records={dayRecords}
onCreate={onCreate}
/>
);
},

View File

@@ -1,12 +1,10 @@
import { useDatabase } from '@/renderer/contexts/database';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { Plus } from 'lucide-react';
import { useView } from '@/renderer/contexts/view';
export const TableViewRecordCreateRow = () => {
const workspace = useWorkspace();
const database = useDatabase();
const { mutate, isPending } = useMutation();
const view = useView();
if (!database.canCreateRecord) {
return null;
@@ -15,20 +13,8 @@ export const TableViewRecordCreateRow = () => {
return (
<button
type="button"
disabled={isPending}
className="animate-fade-in flex h-8 w-full cursor-pointer flex-row items-center gap-1 border-b pl-2 text-muted-foreground hover:bg-gray-50"
onClick={() => {
mutate({
input: {
type: 'record_create',
databaseId: database.id,
userId: workspace.userId,
},
onSuccess: (output) => {
workspace.openInModal(output.id);
},
});
}}
onClick={() => view.createRecord()}
>
<Plus className="size-4" />
<span className="text-sm">Add record</span>

View File

@@ -15,6 +15,7 @@ import { useDatabase } from '@/renderer/contexts/database';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { compareString } from '@/shared/lib/utils';
import {
generateFieldValuesFromFilters,
generateViewFieldIndex,
getDefaultFieldWidth,
getDefaultNameWidth,
@@ -374,6 +375,29 @@ export const View = ({ view }: ViewProps) => {
closeFieldFilter: (fieldId: string) => {
setOpenedFieldFilters((prev) => prev.filter((id) => id !== fieldId));
},
createRecord: (filters?: ViewFilterAttributes[]) => {
const viewFilters = Object.values(view.filters) ?? [];
const extraFilters = filters ?? [];
const allFilters = [...viewFilters, ...extraFilters];
const fields = generateFieldValuesFromFilters(
database.fields,
allFilters,
workspace.userId
);
mutate({
input: {
type: 'record_create',
databaseId: database.id,
userId: workspace.userId,
fields,
},
onSuccess: (output) => {
workspace.openInModal(output.id);
},
});
},
}}
>
{match(view.type)

View File

@@ -15,18 +15,20 @@ export const RecordAttributes = () => {
<RecordName />
</div>
<div className="flex flex-col gap-2">
{database.fields.map((field) => (
<React.Fragment key={field.id}>
<div className="flex flex-row gap-2 h-8">
<div className="w-60 max-w-60">
<RecordField field={field} />
{database.fields
.sort((a, b) => a.index.localeCompare(b.index))
.map((field) => (
<React.Fragment key={field.id}>
<div className="flex flex-row gap-2 h-8">
<div className="w-60 max-w-60">
<RecordField field={field} />
</div>
<div className="flex-1 max-w-128">
<RecordFieldValue field={field} />
</div>
</div>
<div className="flex-1 max-w-128">
<RecordFieldValue field={field} />
</div>
</div>
</React.Fragment>
))}
</React.Fragment>
))}
</div>
</div>
);

View File

@@ -34,6 +34,7 @@ interface ViewContext {
closeSorts: () => void;
openFieldFilter: (fieldId: string) => void;
closeFieldFilter: (fieldId: string) => void;
createRecord: (filters?: ViewFilterAttributes[]) => void;
}
export const ViewContext = createContext<ViewContext>({} as ViewContext);

View File

@@ -9,7 +9,8 @@ const RECORDS_PER_PAGE = 50;
export const useRecordsQuery = (
filters: ViewFilterAttributes[],
sorts: ViewSortAttributes[]
sorts: ViewSortAttributes[],
count?: number
) => {
const workspace = useWorkspace();
const database = useDatabase();
@@ -24,14 +25,15 @@ export const useRecordsQuery = (
filters: filters,
sorts: sorts,
page: i + 1,
count: RECORDS_PER_PAGE,
count: count ?? RECORDS_PER_PAGE,
userId: workspace.userId,
}));
const result = useQueries(inputs);
const records = result.flatMap((data) => data.data ?? []);
const isPending = result.some((data) => data.isPending);
const hasMore = !isPending && records.length === lastPage * RECORDS_PER_PAGE;
const hasMore =
!isPending && records.length === lastPage * (count ?? RECORDS_PER_PAGE);
const loadMore = React.useCallback(() => {
if (hasMore && !isPending) {

View File

@@ -5,6 +5,9 @@ import {
ViewFilterAttributes,
ViewFieldAttributes,
RecordNode,
FieldValue,
MultiSelectFieldAttributes,
SelectFieldAttributes,
} from '@colanode/core';
import { compareString, isStringArray } from '@/shared/lib/utils';
import { generateNodeIndex } from '@/shared/lib/nodes';
@@ -1072,3 +1075,320 @@ export const generateViewFieldIndex = (
return newIndex;
};
export const generateFieldValuesFromFilters = (
fields: FieldAttributes[],
filters: ViewFilterAttributes[],
userId: string
): Record<string, FieldValue> => {
if (fields.length === 0 || filters.length === 0) {
return {};
}
const fieldValues: Record<string, FieldValue> = {};
for (const filter of filters) {
if (filter.type !== 'field') continue;
const field = fields.find((f) => f.id === filter.fieldId);
if (!field) continue;
const value = generateValueFromFilter(field, filter, userId);
if (!value) continue;
fieldValues[field.id] = value;
}
return fieldValues;
};
const generateValueFromFilter = (
field: FieldAttributes,
filter: ViewFieldFilterAttributes,
userId: string
): FieldValue | null => {
switch (field.type) {
case 'boolean': {
return generateBooleanValue(filter);
}
case 'collaborator': {
return generateCollaboratorValue(filter, userId);
}
case 'date': {
return generateDateValue(filter);
}
case 'email': {
return generateEmailValue(filter);
}
case 'file': {
return generateFileValue(filter);
}
case 'multiSelect': {
return generateMultiSelectValue(field, filter);
}
case 'number': {
return generateNumberValue(filter);
}
case 'phone': {
return generatePhoneValue(filter);
}
case 'select': {
return generateSelectValue(field, filter);
}
case 'text': {
return generateTextValue(filter);
}
case 'url': {
return generateUrlValue(filter);
}
default:
return null;
}
};
const generateBooleanValue = (
filter: ViewFieldFilterAttributes
): FieldValue | null => {
if (filter.operator === 'is_true') {
return { type: 'boolean', value: true };
}
if (filter.operator === 'is_false') {
return { type: 'boolean', value: false };
}
return null;
};
const generateCollaboratorValue = (
filter: ViewFieldFilterAttributes,
userId: string
): FieldValue | null => {
if (filter.operator === 'is_me') {
return { type: 'collaborator', value: [userId] };
}
if (
filter.operator === 'is_in' &&
Array.isArray(filter.value) &&
filter.value.length > 0
) {
return { type: 'collaborator', value: [filter.value[0]] };
}
if (filter.operator === 'is_not_empty') {
return { type: 'collaborator', value: [userId] };
}
return null;
};
const generateDateValue = (
filter: ViewFieldFilterAttributes
): FieldValue | null => {
if (typeof filter.value !== 'string') {
return null;
}
if (filter.operator === 'is_equal_to') {
return { type: 'date', value: filter.value };
}
if (filter.operator === 'is_on_or_after') {
return { type: 'date', value: filter.value };
}
if (filter.operator === 'is_on_or_before') {
return { type: 'date', value: filter.value };
}
if (filter.operator === 'is_after') {
const date = new Date(filter.value);
date.setDate(date.getDate() + 1);
return { type: 'date', value: date.toISOString() };
}
if (filter.operator === 'is_before') {
const date = new Date(filter.value);
date.setDate(date.getDate() - 1);
return { type: 'date', value: date.toISOString() };
}
if (filter.operator === 'is_not_empty') {
return { type: 'date', value: new Date().toISOString() };
}
return null;
};
const generateEmailValue = (
filter: ViewFieldFilterAttributes
): FieldValue | null => {
if (typeof filter.value !== 'string') {
return null;
}
if (filter.operator === 'is_equal_to') {
return { type: 'email', value: filter.value };
}
if (filter.operator === 'contains') {
return { type: 'email', value: filter.value };
}
if (filter.operator === 'is_not_empty') {
return { type: 'email', value: '#' };
}
return null;
};
const generateFileValue = (
filter: ViewFieldFilterAttributes
): FieldValue | null => {
if (filter.operator === 'is_in' && Array.isArray(filter.value)) {
return { type: 'file', value: [filter.value[0]] };
}
return null;
};
const generateMultiSelectValue = (
field: MultiSelectFieldAttributes,
filter: ViewFieldFilterAttributes
): FieldValue | null => {
if (filter.operator === 'is_in' && Array.isArray(filter.value)) {
return { type: 'multiSelect', value: [filter.value[0]] };
}
if (
filter.operator === 'is_not_empty' &&
field.options &&
Object.keys(field.options).length > 0
) {
const firstOption = Object.values(field.options)[0];
return { type: 'multiSelect', value: [firstOption.id] };
}
return null;
};
const generateNumberValue = (
filter: ViewFieldFilterAttributes
): FieldValue | null => {
if (typeof filter.value !== 'number') {
return null;
}
if (filter.operator === 'is_equal_to') {
return { type: 'number', value: filter.value };
}
if (filter.operator === 'is_greater_than') {
return { type: 'number', value: filter.value + 1 };
}
if (filter.operator === 'is_less_than') {
return { type: 'number', value: filter.value - 1 };
}
if (filter.operator === 'is_greater_than_or_equal_to') {
return { type: 'number', value: filter.value };
}
if (filter.operator === 'is_less_than_or_equal_to') {
return { type: 'number', value: filter.value };
}
if (filter.operator === 'is_not_empty') {
return { type: 'number', value: 0 };
}
return null;
};
const generatePhoneValue = (
filter: ViewFieldFilterAttributes
): FieldValue | null => {
if (typeof filter.value !== 'string') {
return null;
}
if (filter.operator === 'is_equal_to') {
return { type: 'phone', value: filter.value };
}
if (filter.operator === 'contains') {
return { type: 'phone', value: filter.value };
}
if (filter.operator === 'is_not_empty') {
return { type: 'phone', value: '#' };
}
return null;
};
const generateSelectValue = (
field: SelectFieldAttributes,
filter: ViewFieldFilterAttributes
): FieldValue | null => {
if (filter.operator === 'is_in' && Array.isArray(filter.value)) {
return { type: 'select', value: filter.value[0] };
}
if (
filter.operator === 'is_not_empty' &&
field.options &&
Object.keys(field.options).length > 0
) {
const firstOption = Object.values(field.options)[0];
return { type: 'select', value: firstOption.id };
}
return null;
};
const generateTextValue = (
filter: ViewFieldFilterAttributes
): FieldValue | null => {
if (typeof filter.value !== 'string') {
return null;
}
if (filter.operator === 'is_equal_to') {
return { type: 'text', value: filter.value };
}
if (filter.operator === 'contains') {
return { type: 'text', value: filter.value };
}
if (filter.operator === 'is_not_empty') {
return { type: 'text', value: '#' };
}
return null;
};
const generateUrlValue = (
filter: ViewFieldFilterAttributes
): FieldValue | null => {
if (typeof filter.value !== 'string') {
return null;
}
if (filter.operator === 'is_equal_to') {
return { type: 'url', value: filter.value };
}
if (filter.operator === 'contains') {
return { type: 'url', value: filter.value };
}
if (filter.operator === 'is_not_empty') {
return { type: 'url', value: '#' };
}
return null;
};

View File

@@ -1,8 +1,11 @@
import { FieldValue } from '@colanode/core';
export type RecordCreateMutationInput = {
type: 'record_create';
userId: string;
databaseId: string;
name?: string;
fields?: Record<string, FieldValue>;
};
export type RecordCreateMutationOutput = {