mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Generate default values if a view has filters for records
This commit is contained in:
@@ -19,7 +19,7 @@ export class RecordCreateMutationHandler
|
||||
parentId: input.databaseId,
|
||||
databaseId: input.databaseId,
|
||||
name: input.name ?? '',
|
||||
fields: {},
|
||||
fields: input.fields ?? {},
|
||||
content: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user