Skip to content

Commit f8e4aa2

Browse files
authored
[BST-279] feature: has many association (#328)
1 parent 41e2aab commit f8e4aa2

File tree

39 files changed

+481
-124
lines changed

39 files changed

+481
-124
lines changed

features/data-sources/types.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,11 @@ interface DataQuery extends PrismaDataQuery {
1111
}
1212

1313
export default DataQuery;
14+
15+
export type TableMetaData = {
16+
name: string;
17+
idColumn: string;
18+
nameColumn: string;
19+
createdAtColumn?: string;
20+
updatedAtColumn?: string;
21+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const DummyField = () => null;
2+
3+
export default DummyField;

features/fields/components/FieldWrapper/ShowFieldWrapper.tsx

+27-6
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@ import { ReactNode, memo, useMemo } from "react";
33
import { getColumnNameLabel, iconForField } from "../..";
44
import { isEmpty, isNull, isUndefined } from "lodash";
55
import { useResponsive } from "@/hooks";
6+
import classNames from "classnames";
67

78
const ShowFieldWrapper = ({
89
field,
910
children,
1011
extra,
12+
inline = false,
1113
}: {
12-
field: Field;
14+
field: Field<any>;
1315
children: ReactNode;
1416
extra?: ReactNode;
17+
inline?: boolean;
1518
}) => {
16-
const prettyColumnName = getColumnNameLabel(field?.column?.baseOptions?.label, field?.column?.label, field?.column?.name);
19+
const prettyColumnName = getColumnNameLabel(
20+
field?.column?.baseOptions?.label,
21+
field?.column?.label,
22+
field?.column?.name
23+
);
1724

1825
const IconElement = useMemo(
1926
() => iconForField(field.column),
@@ -23,16 +30,30 @@ const ShowFieldWrapper = ({
2330
const { isMd } = useResponsive();
2431
const showExtra = useMemo(() => {
2532
if (isMd) {
26-
return true;
33+
return !isEmpty(extra);
2734
} else {
2835
return !isUndefined(extra) && !isNull(extra) && !isEmpty(extra);
2936
}
3037
}, [isMd, extra]);
3138

3239
return (
33-
<div className="flex flex-col md:flex-row border-b md:min-h-14 py-3 md:py-0 space-y-3 md:space-y-0 text-sm">
34-
<div className="w-full md:w-48 lg:w-64 xl:w-64 px-4 md:px-6 flex items-start space-x-2">
35-
<div className="flex items-center space-x-2 md:min-h-14 md:py-3">
40+
<div
41+
className={classNames("flex flex-col border-b text-sm py-3 md:py-0", {
42+
"md:flex-col": inline,
43+
"md:flex-row md:min-h-14 space-y-3 md:space-y-0": !inline,
44+
})}
45+
>
46+
<div
47+
className={classNames("w-full flex items-start px-4 space-x-2", {
48+
"md:w-48 lg:w-64 xl:w-64 md:px-6": !inline,
49+
})}
50+
>
51+
<div
52+
className={classNames("flex items-center space-x-2 md:min-h-14", {
53+
"pb-3 md:pb-0 md:pt-3": inline,
54+
"md:py-3": !inline,
55+
})}
56+
>
3657
<IconElement className="h-4 self-start mt-1 lg:self-center lg:mt-0 inline-block flex-shrink-0 text-gray-500" />{" "}
3758
<span>{prettyColumnName}</span>
3859
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ArrowRightIcon } from "@heroicons/react/outline";
2+
import { Tooltip } from "@chakra-ui/react";
3+
import Link from "next/link";
4+
import React, { memo } from "react";
5+
6+
function GoToRecord({
7+
href,
8+
label = "Go to record",
9+
}: {
10+
href: string;
11+
label?: string;
12+
}) {
13+
return (
14+
<Link href={href}>
15+
<a title={label}>
16+
<Tooltip label={label}>
17+
<span className="inline-flex">
18+
<ArrowRightIcon className="inline-block underline text-blue-600 cursor-pointer ml-1 h-4 pt-1" />
19+
</span>
20+
</Tooltip>
21+
</a>
22+
</Link>
23+
);
24+
}
25+
26+
export default memo(GoToRecord);

features/fields/factory.ts

+10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import BooleanShowField from "@/plugins/fields/Boolean/Show";
88
import DateTimeEditField from "@/plugins/fields/DateTime/Edit";
99
import DateTimeIndexField from "@/plugins/fields/DateTime/Index";
1010
import DateTimeShowField from "@/plugins/fields/DateTime/Show";
11+
import DummyField from "./components/DummyField"
1112
import GravatarIndexField from "@/plugins/fields/Gravatar/Index";
1213
import GravatarShowField from "@/plugins/fields/Gravatar/Show";
1314
import IdEditField from "@/plugins/fields/Id/Edit";
@@ -16,6 +17,8 @@ import IdShowField from "@/plugins/fields/Id/Show";
1617
import JsonEditField from "@/plugins/fields/Json/Edit";
1718
import JsonIndexField from "@/plugins/fields/Json/Index";
1819
import JsonShowField from "@/plugins/fields/Json/Show";
20+
import LinkToIndexField from "@/plugins/fields/LinkTo/Index";
21+
import LinkToShowField from "@/plugins/fields/LinkTo/Show";
1922
import NumberEditField from "@/plugins/fields/Number/Edit";
2023
import NumberIndexField from "@/plugins/fields/Number/Index";
2124
import NumberShowField from "@/plugins/fields/Number/Show";
@@ -36,6 +39,7 @@ import type { Column } from "./types";
3639
export const getFieldForEdit = (column: Column) => {
3740
switch (column.fieldType) {
3841
default:
42+
return DummyField;
3943
case "Text":
4044
return TextEditField;
4145
case "Number":
@@ -62,6 +66,7 @@ export const getFieldForEdit = (column: Column) => {
6266
export const getFieldForShow = (column: Column) => {
6367
switch (column.fieldType) {
6468
default:
69+
return DummyField;
6570
case "Text":
6671
return TextShowField;
6772
case "Number":
@@ -80,6 +85,8 @@ export const getFieldForShow = (column: Column) => {
8085
return JsonShowField;
8186
case "Association":
8287
return AssociationShowField;
88+
case "LinkTo":
89+
return LinkToShowField;
8390
case "ProgressBar":
8491
return ProgressBarShowField;
8592
case "Gravatar":
@@ -90,6 +97,7 @@ export const getFieldForShow = (column: Column) => {
9097
export const getFieldForIndex = (column: Column) => {
9198
switch (column.fieldType) {
9299
default:
100+
return DummyField;
93101
case "Id":
94102
return IdIndexField;
95103
case "Text":
@@ -108,6 +116,8 @@ export const getFieldForIndex = (column: Column) => {
108116
return JsonIndexField;
109117
case "Association":
110118
return AssociationIndexField;
119+
case "LinkTo":
120+
return LinkToIndexField;
111121
case "ProgressBar":
112122
return ProgressBarIndexField;
113123
case "Gravatar":

features/fields/index.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@ import {
55
CheckCircleIcon,
66
HashtagIcon,
77
KeyIcon,
8+
LinkIcon,
89
PhotographIcon,
910
SelectorIcon,
1011
TrendingUpIcon,
1112
} from "@heroicons/react/outline";
13+
import { BasetoolRecord } from "../records/types"
1214
import { ElementType } from "react";
1315
import { compact, first, isPlainObject } from "lodash";
1416
import BracketsCurlyIcon from "@/components/svg/BracketsCurlyIcon";
1517
import QuestionIcon from "@/components/svg/QuestionIcon";
1618
import TextIcon from "@/components/svg/TextIcon";
1719
import isArray from "lodash/isArray";
1820
import type { Column, Field, FieldType, FieldValue } from "./types";
19-
import type { Record } from "@/features/records/types";
2021

2122
export const idColumns = ["id", "_id", "ID", "Id"];
2223

@@ -73,6 +74,13 @@ export const getColumnOptions = (
7374
});
7475
}
7576

77+
if (column.baseOptions.computed) {
78+
options.push({
79+
id: "LinkTo",
80+
label: "LinkTo",
81+
});
82+
}
83+
7684
return options;
7785
};
7886

@@ -104,7 +112,7 @@ export const makeField = ({
104112
column,
105113
tableName,
106114
}: {
107-
record: Record;
115+
record: BasetoolRecord;
108116
column: Column;
109117
tableName: string;
110118
}): Field => {
@@ -142,6 +150,8 @@ export const iconForField = (field: Column): ElementType => {
142150
return ArrowRightIcon;
143151
case "ProgressBar":
144152
return TrendingUpIcon;
153+
case "LinkTo":
154+
return LinkIcon;
145155
case "Gravatar":
146156
return PhotographIcon;
147157
}

features/fields/types.d.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Record as BasetoolRecord } from "@/features/records";
1+
import type { BasetoolRecord } from "@/features/records";
22

33
export type FieldType =
44
| "Id"
@@ -11,6 +11,7 @@ export type FieldType =
1111
| "Json"
1212
| "Association"
1313
| "ProgressBar"
14+
| "LinkTo"
1415
| "Gravatar";
1516

1617
export type ForeignKey = {
@@ -65,12 +66,17 @@ export type RecordAssociationValue = {
6566
foreignId: number;
6667
foreignTable: string;
6768
dataSourceId: number;
68-
}
69+
};
6970

70-
export type FieldValue = string | number | undefined | boolean | RecordAssociationValue;
71+
export type FieldValue =
72+
| string
73+
| number
74+
| undefined
75+
| boolean
76+
| RecordAssociationValue;
7177

72-
export type Field = {
73-
value: FieldValue;
78+
export type Field<T = FieldValue> = {
79+
value: T;
7480
column: Column;
7581
record: BasetoolRecord;
7682
tableName: string;

features/records/clientHelpers.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { BasetoolRecord, PossibleRecordValues } from "@/features/records/types";
2+
import { Column } from "../fields/types"
3+
import { IFilter } from "../tables/types";
4+
import { isArray, isString } from "lodash";
5+
6+
export const filtersForHasMany = (
7+
columnName: string,
8+
ids: string | number[]
9+
): IFilter[] => {
10+
let value = "";
11+
12+
if (isArray(ids)) {
13+
value = ids.join(",");
14+
} else if (isString(ids)) {
15+
value = ids;
16+
}
17+
18+
return [
19+
{
20+
column: {} as Column,
21+
columnName,
22+
condition: "is_in",
23+
value,
24+
verb: "and",
25+
},
26+
];
27+
};
28+
29+
/**
30+
* This method tries to extract a pretty name from a record
31+
*/
32+
export const getPrettyName = (
33+
record: BasetoolRecord,
34+
field?: string | undefined
35+
): string => {
36+
// See if we have some `name` column set in the DB
37+
let prettyName: PossibleRecordValues = "";
38+
39+
// Use the `nameColumn` attribute
40+
if (field) {
41+
prettyName = record[field];
42+
} else {
43+
// Try and find a common `name` columns
44+
if (record.url) prettyName = record.url;
45+
if (record.email) prettyName = record.email;
46+
if (record.first_name) prettyName = record.first_name;
47+
if (record.firstName) prettyName = record.firstName;
48+
if (record.title) prettyName = record.title;
49+
if (record.name) prettyName = record.name;
50+
}
51+
52+
if (prettyName) return prettyName.toString();
53+
54+
if (record && record?.id) return record.id.toString();
55+
56+
return "";
57+
};

features/records/components/EditRecord.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Column } from "@/features/fields/types";
2-
import { getVisibleColumns } from "@/features/fields";
2+
import { getConnectedColumns, getVisibleColumns } from "@/features/fields";
33
import { isEmpty, sortBy } from "lodash";
44
import { useACLHelpers } from "@/features/authorization/hooks";
55
import { useDataSourceContext } from "@/hooks";
@@ -42,9 +42,10 @@ const EditRecord = () => {
4242

4343
const columns = useMemo(
4444
() =>
45-
sortBy(getVisibleColumns(columnsResponse?.data, "edit"), [
46-
(column: Column) => column?.baseOptions?.orderIndex,
47-
]),
45+
sortBy(
46+
getConnectedColumns(getVisibleColumns(columnsResponse?.data, "edit")),
47+
[(column: Column) => column?.baseOptions?.orderIndex]
48+
),
4849
[columnsResponse?.data]
4950
);
5051

features/records/components/Form.tsx

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BasetoolRecord } from "../types";
12
import { Button } from "@chakra-ui/button";
23
import { Column } from "@/features/fields/types";
34
import { PencilAltIcon } from "@heroicons/react/outline";
@@ -16,16 +17,15 @@ import { useDataSourceContext } from "@/hooks";
1617
import { useForm } from "react-hook-form";
1718
import { useRouter } from "next/router";
1819
import BackButton from "./BackButton";
19-
import Joi, { ObjectSchema } from "joi";
20+
import Joi, { ObjectSchema, SchemaLike } from "joi";
2021
import LoadingOverlay from "@/components/LoadingOverlay";
2122
import PageWrapper from "@/components/PageWrapper";
2223
import React, { memo, useEffect, useMemo, useState } from "react";
2324
import isUndefined from "lodash/isUndefined";
2425
import logger from "@/lib/logger";
25-
import type { Record } from "@/features/records/types";
2626

27-
const makeSchema = async (record: Record, columns: Column[]) => {
28-
const schema: { [columnName: string]: any } = {};
27+
const makeSchema = async (record: BasetoolRecord, columns: Column[]) => {
28+
const schema: { [columnName: string]: SchemaLike } = {};
2929

3030
// eslint-disable-next-line no-restricted-syntax
3131
for (const column of columns) {
@@ -57,7 +57,7 @@ const Form = ({
5757
columns,
5858
formForCreate = false,
5959
}: {
60-
record: Record;
60+
record: BasetoolRecord;
6161
columns: Column[];
6262
formForCreate?: boolean;
6363
}) => {
@@ -89,14 +89,14 @@ const Form = ({
8989
.map(([columnName, value]) => {
9090
// Add foreignId for association columns
9191
if (associationColumnNames.includes(columnName)) {
92-
return [columnName, value.foreignId];
92+
return [columnName, (value as any)?.foreignId];
9393
}
9494

9595
return [columnName, value];
9696
})
9797
.filter(([columnName]) => {
9898
// Remove invisible columns
99-
if (visibleColumnNames.includes(columnName)) {
99+
if (visibleColumnNames.includes(columnName?.toString() || "")) {
100100
return true;
101101
}
102102

@@ -251,7 +251,7 @@ const Form = ({
251251
if (!formData) return null;
252252

253253
const field = makeField({
254-
record: formData as Record,
254+
record: formData as BasetoolRecord,
255255
column,
256256
tableName: tableName,
257257
});

0 commit comments

Comments
 (0)