tsondb 0.12.5 → 0.12.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,13 @@ import type { NumberConstraints } from "../../../../shared/validation/number.ts"
2
2
  import type { GetNestedDeclarations, GetReferences, Predicate, Serializer, TypeArgumentsResolver, Validator } from "../../Node.ts";
3
3
  import { NodeKind } from "../../Node.ts";
4
4
  import type { BaseType, StructureFormatter } from "../Type.ts";
5
- export interface FloatType extends BaseType, NumberConstraints {
5
+ export interface FloatConstraints extends NumberConstraints {
6
+ fractionDigits?: number;
7
+ }
8
+ export interface FloatType extends BaseType, FloatConstraints {
6
9
  kind: NodeKind["FloatType"];
7
10
  }
8
- export declare const FloatType: (options?: NumberConstraints) => FloatType;
11
+ export declare const FloatType: (options?: FloatConstraints) => FloatType;
9
12
  export { FloatType as Float };
10
13
  export declare const isFloatType: Predicate<FloatType>;
11
14
  export declare const getNestedDeclarationsInFloatType: GetNestedDeclarations<FloatType>;
@@ -1,10 +1,16 @@
1
1
  import { validateNumberConstraints } from "../../../../shared/validation/number.js";
2
2
  import { json } from "../../../utils/errorFormatting.js";
3
3
  import { NodeKind } from "../../Node.js";
4
- export const FloatType = (options = {}) => ({
5
- ...options,
6
- kind: NodeKind.FloatType,
7
- });
4
+ export const FloatType = (options = {}) => {
5
+ if (options.fractionDigits !== undefined &&
6
+ (!Number.isInteger(options.fractionDigits) || options.fractionDigits < 1)) {
7
+ throw new TypeError("The fractionDigits option must be a positive integer");
8
+ }
9
+ return {
10
+ ...options,
11
+ kind: NodeKind.FloatType,
12
+ };
13
+ };
8
14
  export { FloatType as Float };
9
15
  export const isFloatType = node => node.kind === NodeKind.FloatType;
10
16
  export const getNestedDeclarationsInFloatType = addedDecls => addedDecls;
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { hasFileChanges, splitBranchName } from "../../../shared/utils/git.js";
5
5
  import { getInstanceContainerOverview } from "../../../shared/utils/instances.js";
6
6
  import { getGroupedInstancesFromDatabaseInMemory } from "../../utils/databaseInMemory.js";
7
+ import { attachGitStatusToDatabaseInMemory } from "../../utils/git.js";
7
8
  import { reinit } from "../init.js";
8
9
  import { createChildInstancesForInstanceIdGetter } from "../utils/childInstances.js";
9
10
  const debug = Debug("tsondb:server:api:git");
@@ -24,6 +25,9 @@ gitApi.get("/", (req, res) => {
24
25
  });
25
26
  gitApi.get("/status", async (req, res) => {
26
27
  const status = await req.git.status();
28
+ req.setLocal("databaseInMemory", attachGitStatusToDatabaseInMemory(req.databaseInMemory, req.dataRoot,
29
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
30
+ req.gitRoot, status));
27
31
  const getChildInstancesForInstanceId = createChildInstancesForInstanceIdGetter(req);
28
32
  const body = {
29
33
  currentBranch: status.current,
@@ -2,10 +2,12 @@ import express from "express";
2
2
  import { declarationsApi } from "./declarations.js";
3
3
  import { gitApi } from "./git.js";
4
4
  import { instancesApi } from "./instances.js";
5
+ import { searchApi } from "./search.js";
5
6
  export const api = express.Router();
6
7
  api.use("/declarations", declarationsApi);
7
8
  api.use("/instances", instancesApi);
8
9
  api.use("/git", gitApi);
10
+ api.use("/search", searchApi);
9
11
  api.get("/config", (req, res) => {
10
12
  const body = {
11
13
  localeEntityName: req.localeEntity?.name,
@@ -0,0 +1 @@
1
+ export declare const searchApi: import("express-serve-static-core").Router;
@@ -0,0 +1,50 @@
1
+ import Debug from "debug";
2
+ import express from "express";
3
+ import { isEntityDeclWithParentReference } from "../../schema/index.js";
4
+ import { getGroupedInstancesFromDatabaseInMemory } from "../../utils/databaseInMemory.js";
5
+ import { getDisplayNameFromEntityInstance } from "../../utils/displayName.js";
6
+ import { createChildInstancesForInstanceIdGetter } from "../utils/childInstances.js";
7
+ import { getQueryParamString } from "../utils/query.js";
8
+ const debug = Debug("tsondb:server:api:search");
9
+ export const searchApi = express.Router();
10
+ searchApi.get("/", (req, res) => {
11
+ const query = getQueryParamString(req.query, "q")?.toLowerCase() ?? "";
12
+ debug('search for items containing "%s"', query);
13
+ const getChildInstancesForInstanceId = createChildInstancesForInstanceIdGetter(req);
14
+ const body = {
15
+ query,
16
+ results: query.length === 0
17
+ ? []
18
+ : getGroupedInstancesFromDatabaseInMemory(req.databaseInMemory)
19
+ .flatMap(([entityName, instances]) => {
20
+ const entity = req.entitiesByName[entityName];
21
+ if (entity && isEntityDeclWithParentReference(entity)) {
22
+ return [];
23
+ }
24
+ return instances
25
+ .map((instance) => {
26
+ const { name, localeId } = getDisplayNameFromEntityInstance(
27
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
28
+ req.entitiesByName[entityName], instance, req.getInstanceById, getChildInstancesForInstanceId, req.locales);
29
+ const searchableName = name.toLowerCase();
30
+ return [
31
+ entityName,
32
+ {
33
+ id: instance.id,
34
+ displayName: name,
35
+ displayNameLocaleId: localeId,
36
+ relevance: instance.id.startsWith(query) || searchableName.startsWith(query)
37
+ ? 1
38
+ : instance.id.includes(query) || searchableName.includes(query)
39
+ ? 0.5
40
+ : 0,
41
+ },
42
+ ];
43
+ })
44
+ .filter(instance => instance[1].relevance > 0);
45
+ })
46
+ .toSorted((a, b) => a[1].relevance - b[1].relevance ||
47
+ a[1].displayName.localeCompare(b[1].displayName, a[1].displayNameLocaleId)),
48
+ };
49
+ res.json(body);
50
+ });
@@ -8,7 +8,7 @@ export const createInstance = async (locals, instance, idQueryParam) => {
8
8
  if (entity === undefined) {
9
9
  return error(new HTTPError(400, "Entity not found"));
10
10
  }
11
- const databaseTransactionResult = await runDatabaseTransaction(locals.dataRoot, locals.gitRoot ? locals.git : undefined, locals.entitiesByName, locals.databaseInMemory, locals.referencesToInstances, res => saveInstanceTree(locals.entitiesByName, undefined, locals.localeEntity, instance.entityName, undefined, instance, idQueryParam, res));
11
+ const databaseTransactionResult = await runDatabaseTransaction(locals.dataRoot, locals.gitRoot ? locals.git : undefined, locals.entitiesByName, locals.databaseInMemory, locals.referencesToInstances, res => saveInstanceTree(locals.entitiesByName, undefined, undefined, locals.localeEntity, instance.entityName, undefined, instance, idQueryParam, res));
12
12
  if (isError(databaseTransactionResult)) {
13
13
  return databaseTransactionResult;
14
14
  }
@@ -27,7 +27,7 @@ export const updateInstance = async (locals, instance) => {
27
27
  return error(new HTTPError(400, "Entity not found"));
28
28
  }
29
29
  const oldChildInstances = getChildInstances(locals.databaseInMemory, entity, instance.id, true);
30
- const databaseTransactionResult = await runDatabaseTransaction(locals.dataRoot, locals.gitRoot ? locals.git : undefined, locals.entitiesByName, locals.databaseInMemory, locals.referencesToInstances, res => saveInstanceTree(locals.entitiesByName, undefined, locals.localeEntity, instance.entityName, {
30
+ const databaseTransactionResult = await runDatabaseTransaction(locals.dataRoot, locals.gitRoot ? locals.git : undefined, locals.entitiesByName, locals.databaseInMemory, locals.referencesToInstances, res => saveInstanceTree(locals.entitiesByName, undefined, undefined, locals.localeEntity, instance.entityName, {
31
31
  id: instance.id,
32
32
  content: instanceContainer.content,
33
33
  childInstances: oldChildInstances,
@@ -51,7 +51,7 @@ export const deleteInstance = async (locals, entityName, instanceId) => {
51
51
  return error(new HTTPError(400, "Entity not found"));
52
52
  }
53
53
  const oldChildInstances = getChildInstances(locals.databaseInMemory, entity, instanceId, true);
54
- const databaseTransactionResult = await runDatabaseTransaction(locals.dataRoot, locals.gitRoot ? locals.git : undefined, locals.entitiesByName, locals.databaseInMemory, locals.referencesToInstances, res => saveInstanceTree(locals.entitiesByName, undefined, locals.localeEntity, entityName, {
54
+ const databaseTransactionResult = await runDatabaseTransaction(locals.dataRoot, locals.gitRoot ? locals.git : undefined, locals.entitiesByName, locals.databaseInMemory, locals.referencesToInstances, res => saveInstanceTree(locals.entitiesByName, undefined, undefined, locals.localeEntity, entityName, {
55
55
  id: instanceId,
56
56
  content: instanceContainer.content,
57
57
  childInstances: oldChildInstances,
@@ -0,0 +1,2 @@
1
+ import type { Request } from "express";
2
+ export declare const getQueryParamString: (parsedQueryString: Request["query"], key: string) => string | undefined;
@@ -0,0 +1,7 @@
1
+ export const getQueryParamString = (parsedQueryString, key) => {
2
+ const value = parsedQueryString[key];
3
+ if (typeof value === "string") {
4
+ return value;
5
+ }
6
+ return;
7
+ };
@@ -29,6 +29,6 @@ export interface GenEntityTaggedInstanceContainerWithChildInstances<ID extends s
29
29
  }
30
30
  export declare const getChildInstancesFromEntity: (databaseInMemory: DatabaseInMemory, parentEntity: EntityDecl, parentId: string, childEntity: EntityDeclWithParentReference) => InstanceContainer[];
31
31
  export declare const getChildInstances: (databaseInMemory: DatabaseInMemory, parentEntity: EntityDecl, parentId: string, recursive?: boolean) => EntityTaggedInstanceContainerWithChildInstances[];
32
- export declare const saveInstanceTree: (entitiesByName: Record<string, EntityDecl>, parentId: string | undefined, localeEntity: EntityDecl | undefined, entityName: string, oldInstance: EntityTaggedInstanceContainerWithChildInstances | undefined, newInstance: UnsafeEntityTaggedInstanceContainerWithChildInstances | undefined, customId: unknown, res: TransactionResult) => TransactionResult<{
32
+ export declare const saveInstanceTree: (entitiesByName: Record<string, EntityDecl>, parentEntityName: string | undefined, parentId: string | undefined, localeEntity: EntityDecl | undefined, entityName: string, oldInstance: EntityTaggedInstanceContainerWithChildInstances | undefined, newInstance: UnsafeEntityTaggedInstanceContainerWithChildInstances | undefined, customId: unknown, res: TransactionResult) => TransactionResult<{
33
33
  instanceContainer: InstanceContainer;
34
34
  }>;
@@ -1,6 +1,7 @@
1
+ import { createEnumCaseValue } from "../../shared/schema/declarations/EnumDecl.js";
1
2
  import { hasKey } from "../../shared/utils/object.js";
2
3
  import { error, isError, map, ok } from "../../shared/utils/result.js";
3
- import { isEntityDeclWithParentReference, reduceNodes } from "../schema/index.js";
4
+ import { isEntityDeclWithParentReference, isEnumDecl, isIncludeIdentifierType, reduceNodes, } from "../schema/index.js";
4
5
  import { isChildEntitiesType } from "../schema/types/references/ChildEntitiesType.js";
5
6
  import { getInstancesOfEntityFromDatabaseInMemory, } from "./databaseInMemory.js";
6
7
  import { HTTPError } from "./error.js";
@@ -30,21 +31,26 @@ export const getChildInstances = (databaseInMemory, parentEntity, parentId, recu
30
31
  : [],
31
32
  })));
32
33
  };
33
- const prepareNewChildInstanceContent = (entity, parentId, content) => {
34
+ const prepareNewChildInstanceContent = (entity, parentEntityName, parentId, content) => {
34
35
  if (isEntityDeclWithParentReference(entity)) {
35
- if (parentId === undefined) {
36
+ if (parentEntityName === undefined || parentId === undefined) {
36
37
  return error(new HTTPError(400, `Cannot create instance of child entity "${entity.name}" without parent reference`));
37
38
  }
39
+ const parentReferenceType = entity.type.value.properties[entity.parentReferenceKey]?.type;
38
40
  return ok({
39
41
  ...content,
40
- [entity.parentReferenceKey]: parentId,
42
+ [entity.parentReferenceKey]: parentReferenceType &&
43
+ isIncludeIdentifierType(parentReferenceType) &&
44
+ isEnumDecl(parentReferenceType.reference)
45
+ ? createEnumCaseValue(parentEntityName, parentId)
46
+ : parentId,
41
47
  });
42
48
  }
43
49
  else {
44
50
  return ok(content);
45
51
  }
46
52
  };
47
- export const saveInstanceTree = (entitiesByName, parentId, localeEntity, entityName, oldInstance, newInstance, customId, res) => {
53
+ export const saveInstanceTree = (entitiesByName, parentEntityName, parentId, localeEntity, entityName, oldInstance, newInstance, customId, res) => {
48
54
  if (isError(res)) {
49
55
  return res;
50
56
  }
@@ -59,14 +65,14 @@ export const saveInstanceTree = (entitiesByName, parentId, localeEntity, entityN
59
65
  }
60
66
  // delete all child instances recursively
61
67
  const deletedRes = deleteInstance(res, entity, oldInstance.id);
62
- return map(oldInstance.childInstances.reduce((resAcc, oldChildInstance) => saveInstanceTree(entitiesByName, oldInstance.id, localeEntity, oldInstance.entityName, oldChildInstance, undefined, undefined, resAcc), deletedRes), data => ({
68
+ return map(oldInstance.childInstances.reduce((resAcc, oldChildInstance) => saveInstanceTree(entitiesByName, oldInstance.entityName, oldInstance.id, localeEntity, oldChildInstance.entityName, oldChildInstance, undefined, undefined, resAcc), deletedRes), data => ({
63
69
  ...data,
64
70
  instanceContainer: { id: oldInstance.id, content: oldInstance.content },
65
71
  }));
66
72
  }
67
73
  else {
68
74
  const preparedContent = newInstance.id === undefined
69
- ? prepareNewChildInstanceContent(entity, parentId, newInstance.content)
75
+ ? prepareNewChildInstanceContent(entity, parentEntityName, parentId, newInstance.content)
70
76
  : ok(newInstance.content);
71
77
  if (isError(preparedContent)) {
72
78
  return preparedContent;
@@ -85,8 +91,8 @@ export const saveInstanceTree = (entitiesByName, parentId, localeEntity, entityN
85
91
  const setResWithoutInfo = ok({ ...setRes.value, additionalInformation: undefined });
86
92
  return map(newInstance.childInstances
87
93
  .filter(newChildInstance => newChildInstance.id === undefined)
88
- .reduce((resAcc, newChildInstance) => saveInstanceTree(entitiesByName, instanceId, localeEntity, newChildInstance.entityName, undefined, newChildInstance, undefined, resAcc), oldInstance
89
- ? oldInstance.childInstances.reduce((resAcc, oldChildInstance) => saveInstanceTree(entitiesByName, instanceId, localeEntity, oldChildInstance.entityName, oldChildInstance, newInstance.childInstances.find(ci => ci.id === oldChildInstance.id), undefined, resAcc), setResWithoutInfo)
94
+ .reduce((resAcc, newChildInstance) => saveInstanceTree(entitiesByName, newInstance.entityName, instanceId, localeEntity, newChildInstance.entityName, undefined, newChildInstance, undefined, resAcc), oldInstance
95
+ ? oldInstance.childInstances.reduce((resAcc, oldChildInstance) => saveInstanceTree(entitiesByName, oldInstance.entityName, instanceId, localeEntity, oldChildInstance.entityName, oldChildInstance, newInstance.childInstances.find(ci => ci.id === oldChildInstance.id), undefined, resAcc), setResWithoutInfo)
90
96
  : setResWithoutInfo), data => ({
91
97
  ...data,
92
98
  instanceContainer: { id: instanceId, content: preparedContent.value },
@@ -87,3 +87,7 @@ export interface CreateCommitRequestBody {
87
87
  export interface CreateBranchRequestBody {
88
88
  branchName: string;
89
89
  }
90
+ export interface SearchResponseBody {
91
+ query: string;
92
+ results: [entityName: string, instane: InstanceContainerOverview][];
93
+ }
@@ -10,3 +10,11 @@ export interface SerializedEnumDecl<Name extends string = string, T extends Reco
10
10
  export declare const isSerializedEnumDecl: (node: SerializedNode) => node is SerializedEnumDecl;
11
11
  export declare const resolveTypeArgumentsInSerializedEnumDecl: SerializedTypeArgumentsResolver<SerializedEnumDecl>;
12
12
  export declare const getReferencesForSerializedEnumDecl: GetReferencesSerialized<SerializedEnumDecl>;
13
+ export declare const ENUM_DISCRIMINATOR_KEY = "kind";
14
+ export type ENUM_DISCRIMINATOR_KEY = typeof ENUM_DISCRIMINATOR_KEY;
15
+ export type EnumValue<K extends string, V> = {
16
+ [ENUM_DISCRIMINATOR_KEY]: K;
17
+ } & {
18
+ [Key2 in K]: V;
19
+ };
20
+ export declare const createEnumCaseValue: <K extends string, V>(caseName: K, caseValue: V) => EnumValue<K, V>;
@@ -9,3 +9,8 @@ export const resolveTypeArgumentsInSerializedEnumDecl = (decls, args, decl) => {
9
9
  };
10
10
  };
11
11
  export const getReferencesForSerializedEnumDecl = (decls, decl, value) => getReferencesForSerializedEnumType(decls, decl.type, value);
12
+ export const ENUM_DISCRIMINATOR_KEY = "kind";
13
+ export const createEnumCaseValue = (caseName, caseValue) => ({
14
+ [ENUM_DISCRIMINATOR_KEY]: caseName,
15
+ [caseName]: caseValue,
16
+ });
@@ -1,11 +1,12 @@
1
- import type { RangeBound } from "../../validation/number.ts";
1
+ import type { NumberConstraints } from "../../validation/number.ts";
2
2
  import type { GetReferencesSerialized, NodeKind, SerializedTypeArgumentsResolver } from "../Node.ts";
3
3
  import type { SerializedBaseType } from "./Type.ts";
4
- export interface SerializedFloatType extends SerializedBaseType {
4
+ export interface FloatConstraints extends NumberConstraints {
5
+ fractionDigits?: number;
6
+ }
7
+ export declare const DEFAULT_FRACTION_DIGITS = 2;
8
+ export interface SerializedFloatType extends SerializedBaseType, FloatConstraints {
5
9
  kind: NodeKind["FloatType"];
6
- minimum?: RangeBound;
7
- maximum?: RangeBound;
8
- multipleOf?: number;
9
10
  }
10
11
  export declare const resolveTypeArgumentsInSerializedFloatType: SerializedTypeArgumentsResolver<SerializedFloatType>;
11
12
  export declare const getReferencesForSerializedFloatType: GetReferencesSerialized<SerializedFloatType>;
@@ -1,2 +1,3 @@
1
+ export const DEFAULT_FRACTION_DIGITS = 2;
1
2
  export const resolveTypeArgumentsInSerializedFloatType = (_decls, _args, type) => type;
2
3
  export const getReferencesForSerializedFloatType = () => [];
@@ -0,0 +1,2 @@
1
+ import type { SearchResponseBody } from "../../shared/api.ts";
2
+ export declare const searchInstances: (locales: string[], query: string) => Promise<SearchResponseBody>;
@@ -0,0 +1,7 @@
1
+ import { getResource } from "../utils/api.js";
2
+ export const searchInstances = async (locales, query) => getResource("/api/search", {
3
+ locales,
4
+ modifyUrl: url => {
5
+ url.searchParams.set("q", query);
6
+ },
7
+ });
@@ -6,7 +6,7 @@ import { Settings } from "./Settings.js";
6
6
  export const Layout = ({ breadcrumbs, children }) => {
7
7
  const [_1, setIsGitOpen] = useContext(GitContext);
8
8
  const [isGitAlwaysOpen, _2] = useSetting("gitSidebar");
9
- return (_jsxs(_Fragment, { children: [_jsxs("header", { children: [_jsx("nav", { children: _jsx("ol", { children: breadcrumbs.map(({ url, label }) => (_jsx("li", { children: _jsx("a", { href: url, children: label }) }, url))) }) }), _jsxs("div", { class: "nav-buttons", children: [_jsx("button", { class: `git-toggle${!isGitAlwaysOpen ? " git-toggle--no-sidebar" : ""}`, onClick: () => {
9
+ return (_jsxs(_Fragment, { children: [_jsxs("header", { children: [_jsx("nav", { children: _jsx("ol", { children: breadcrumbs.map(({ url, label }) => (_jsx("li", { children: _jsx("a", { href: url, children: label }) }, url))) }) }), _jsxs("div", { class: "nav-buttons", children: [_jsx("a", { href: "/search", class: "btn", children: "Search" }), _jsx("button", { class: `git-toggle${!isGitAlwaysOpen ? " git-toggle--no-sidebar" : ""}`, onClick: () => {
10
10
  setIsGitOpen(b => !b);
11
11
  }, children: "File changes" }), _jsx(Settings, {})] })] }), _jsx("main", { children: children })] }));
12
12
  };
@@ -2,5 +2,5 @@ import { jsx as _jsx } from "preact/jsx-runtime";
2
2
  import { getGitStatusForDisplay, getLabelForGitStatus, } from "../../../shared/utils/git.js";
3
3
  export const GitStatusIndicator = ({ status }) => {
4
4
  const gitStatusForDisplay = getGitStatusForDisplay(status);
5
- return (_jsx("span", { class: `git-status git-status--${gitStatusForDisplay ?? ""}`, title: getLabelForGitStatus(gitStatusForDisplay), children: gitStatusForDisplay }));
5
+ return gitStatusForDisplay === undefined ? null : (_jsx("span", { class: `git-status git-status--${gitStatusForDisplay}`, title: getLabelForGitStatus(gitStatusForDisplay), children: gitStatusForDisplay }));
6
6
  };
@@ -1,5 +1,5 @@
1
1
  import type { FunctionComponent } from "preact";
2
- import type { SerializedFloatType } from "../../../shared/schema/types/FloatType.ts";
2
+ import { type SerializedFloatType } from "../../../shared/schema/types/FloatType.ts";
3
3
  import type { TypeInputProps } from "./TypeInput.tsx";
4
4
  type Props = TypeInputProps<SerializedFloatType, number>;
5
5
  export declare const FloatTypeInput: FunctionComponent<Props>;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
2
  import { useState } from "preact/hooks";
3
+ import { DEFAULT_FRACTION_DIGITS, } from "../../../shared/schema/types/FloatType.js";
3
4
  import { validateNumberConstraints } from "../../../shared/validation/number.js";
4
5
  import { MismatchingTypeError } from "./utils/MismatchingTypeError.js";
5
6
  import { ValidationErrors } from "./utils/ValidationErrors.js";
@@ -15,5 +16,5 @@ export const FloatTypeInput = ({ type, value, disabled, onChange }) => {
15
16
  if (!Number.isNaN(numericValue)) {
16
17
  onChange(numericValue);
17
18
  }
18
- }, step: 0.01, "aria-invalid": errors.length > 0, disabled: disabled }), _jsx(ValidationErrors, { disabled: disabled, errors: errors })] }));
19
+ }, step: 1 / Math.pow(10, type.fractionDigits ?? DEFAULT_FRACTION_DIGITS), "aria-invalid": errors.length > 0, disabled: disabled }), _jsx(ValidationErrors, { disabled: disabled, errors: errors })] }));
19
20
  };
@@ -20,6 +20,7 @@ import { Entity } from "./routes/Entity.js";
20
20
  import { Home } from "./routes/Home.js";
21
21
  import { Instance } from "./routes/Instance.js";
22
22
  import { NotFound } from "./routes/NotFound.js";
23
+ import { Search } from "./routes/Search.js";
23
24
  const mapEntities = (data) => data.declarations
24
25
  .map(decl => ({ ...decl, isLocaleEntity: decl.declaration.name === data.localeEntity }))
25
26
  .sort((a, b) => a.declaration.name.localeCompare(b.declaration.name));
@@ -33,7 +34,7 @@ const App = ({ config }) => {
33
34
  alert("Error reloading entities: " + String(error));
34
35
  });
35
36
  }, [location.path, reloadEntities]);
36
- return (_jsx(ConfigContext.Provider, { value: config, children: _jsx(SettingsContext.Provider, { value: settingsContext, children: _jsx(GitContext.Provider, { value: [isGitOpen, setIsGitOpen], children: _jsx(LocationProvider, { children: _jsx(EntitiesContext.Provider, { value: { entities: entities ?? [], reloadEntities }, children: _jsxs(ContextProviderWrapper, { context: GitClientContext, useValue: useGitClient, children: [_jsx(LoadingOverlay, {}), _jsxs(Router, { children: [_jsx(Route, { path: "/", component: Home }), _jsx(Route, { path: "/entities/:name", component: Entity }), _jsx(Route, { path: "/entities/:name/instances/create", component: CreateInstance }), _jsx(Route, { path: "/entities/:name/instances/:id", component: Instance }), _jsx(Route, { default: true, component: NotFound })] }), _jsx(Git, {})] }) }) }) }) }) }));
37
+ return (_jsx(ConfigContext.Provider, { value: config, children: _jsx(SettingsContext.Provider, { value: settingsContext, children: _jsx(GitContext.Provider, { value: [isGitOpen, setIsGitOpen], children: _jsx(LocationProvider, { children: _jsx(EntitiesContext.Provider, { value: { entities: entities ?? [], reloadEntities }, children: _jsxs(ContextProviderWrapper, { context: GitClientContext, useValue: useGitClient, children: [_jsx(LoadingOverlay, {}), _jsxs(Router, { children: [_jsx(Route, { path: "/", component: Home }), _jsx(Route, { path: "/search", component: Search }), _jsx(Route, { path: "/entities/:name", component: Entity }), _jsx(Route, { path: "/entities/:name/instances/create", component: CreateInstance }), _jsx(Route, { path: "/entities/:name/instances/:id", component: Instance }), _jsx(Route, { default: true, component: NotFound })] }), _jsx(Git, {})] }) }) }) }) }) }));
37
38
  };
38
39
  const config = await getWebConfig();
39
40
  const root = document.getElementById("app");
@@ -0,0 +1,2 @@
1
+ import type { FunctionalComponent } from "preact";
2
+ export declare const Search: FunctionalComponent;
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from "preact/hooks";
3
+ import { getGitStatusForDisplay } from "../../shared/utils/git.js";
4
+ import { toTitleCase } from "../../shared/utils/string.js";
5
+ import { deleteInstanceByEntityNameAndId } from "../api/declarations.js";
6
+ import { searchInstances } from "../api/search.js";
7
+ import { GitStatusIndicator } from "../components/git/GitStatusIndicator.js";
8
+ import { Layout } from "../components/Layout.js";
9
+ import { useSetting } from "../hooks/useSettings.js";
10
+ import { logAndAlertError } from "../utils/debug.js";
11
+ import { homeTitle } from "./Home.js";
12
+ const MIN_CHARACTERS = 3;
13
+ export const Search = () => {
14
+ const [locales] = useSetting("displayedLocales");
15
+ const [query, setQuery] = useState(() => new URLSearchParams(window.location.search).get("q") ?? "");
16
+ const [results, setResults] = useState();
17
+ const search = useCallback(() => {
18
+ const url = new URL(window.location.href);
19
+ if (url.searchParams.get("q") !== query) {
20
+ if (query.length === 0) {
21
+ url.searchParams.delete("q");
22
+ }
23
+ else {
24
+ url.searchParams.set("q", query);
25
+ }
26
+ window.history.pushState({}, "", url);
27
+ }
28
+ if (query.length >= MIN_CHARACTERS) {
29
+ searchInstances(locales, query)
30
+ .then(res => {
31
+ setResults(res.results);
32
+ })
33
+ .catch(logAndAlertError);
34
+ }
35
+ }, [locales, query]);
36
+ useEffect(() => {
37
+ document.title = "Search — TSONDB";
38
+ search();
39
+ }, [search]);
40
+ return (_jsxs(Layout, { breadcrumbs: [{ url: "/", label: homeTitle }], children: [_jsx("h1", { children: "Search" }), _jsx("input", { type: "search", name: "q", value: query, onInput: event => {
41
+ setQuery(event.currentTarget.value);
42
+ } }), query.length < MIN_CHARACTERS ? (_jsx("p", { class: "help", children: "Provide at least 3 characters in the search field to start the search" })) : (results && (_jsxs("section", { class: "search-results", children: [_jsx("h2", { children: "Results" }), results.length === 0 ? (_jsx("p", { class: "empty", children: "No results" })) : (_jsx("ul", { class: "entries entries--instances", children: results.map(([entityName, instance]) => {
43
+ const gitStatusForDisplay = getGitStatusForDisplay(instance.gitStatus);
44
+ return (_jsxs("li", { id: `instance-${instance.id}`, class: `entries-item${gitStatusForDisplay === undefined
45
+ ? ""
46
+ : ` git-status--${gitStatusForDisplay}`}`, children: [_jsxs("h2", { class: "entries-item__title", children: [instance.displayName, _jsx("span", { "aria-hidden": true, class: "entries-item__title-entity", children: toTitleCase(entityName) })] }), _jsx("p", { "aria-hidden": true, class: "entries-item__subtitle entries-item__subtitle--id", children: instance.id }), _jsxs("div", { class: "entries-item__side", children: [_jsx(GitStatusIndicator, { status: instance.gitStatus }), _jsxs("div", { class: "btns", children: [_jsx("a", { href: `/entities/${entityName}/instances/${instance.id}`, class: "btn", children: "Edit" }), _jsx("button", { class: "destructive", onClick: () => {
47
+ if (confirm("Are you sure you want to delete this instance?")) {
48
+ deleteInstanceByEntityNameAndId(locales, entityName, instance.id)
49
+ .then(() => {
50
+ search();
51
+ })
52
+ .catch(logAndAlertError);
53
+ }
54
+ }, children: "Delete" })] })] })] }, instance.id));
55
+ }) }))] })))] }));
56
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsondb",
3
- "version": "0.12.5",
3
+ "version": "0.12.6",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "Lukas Obermann",
@@ -589,6 +589,12 @@ select {
589
589
  }
590
590
  }
591
591
 
592
+ .entries-item__title-entity {
593
+ font-weight: 400;
594
+ color: var(--color-foreground-secondary);
595
+ padding-left: 0.5em;
596
+ }
597
+
592
598
  .entries-item__subtitle--id {
593
599
  font-family: var(--font-mono);
594
600
  }
@@ -1205,3 +1211,7 @@ dialog[open]:has(> .loading-overlay--open)::backdrop {
1205
1211
  .object-item--translation button {
1206
1212
  align-self: center;
1207
1213
  }
1214
+
1215
+ section.search-results {
1216
+ margin-top: 3.6rem;
1217
+ }