@jskit-ai/agent-docs 0.1.4 → 0.1.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.
- package/DISTR_AGENT.md +2 -0
- package/guide/agent/generators/advanced-cruds.md +307 -51
- package/package.json +2 -1
- package/patterns/INDEX.md +33 -0
- package/patterns/child-cruds.md +30 -0
- package/patterns/crud-links.md +36 -0
- package/patterns/filters.md +92 -0
- package/patterns/live-actions.md +33 -0
- package/patterns/placements.md +28 -0
- package/patterns/surfaces.md +30 -0
- package/reference/autogen/KERNEL_MAP.md +27 -0
- package/reference/autogen/packages/agent-docs.md +1 -1
- package/reference/autogen/packages/assistant-core.md +1 -1
- package/reference/autogen/packages/assistant-runtime.md +22 -9
- package/reference/autogen/packages/assistant.md +1 -1
- package/reference/autogen/packages/auth-core.md +1 -1
- package/reference/autogen/packages/auth-provider-supabase-core.md +1 -1
- package/reference/autogen/packages/auth-web.md +1 -1
- package/reference/autogen/packages/console-core.md +1 -1
- package/reference/autogen/packages/console-web.md +1 -1
- package/reference/autogen/packages/crud-core.md +23 -1
- package/reference/autogen/packages/crud-server-generator.md +1 -1
- package/reference/autogen/packages/crud-ui-generator.md +2 -1
- package/reference/autogen/packages/database-runtime-mysql.md +1 -1
- package/reference/autogen/packages/database-runtime-postgres.md +1 -1
- package/reference/autogen/packages/database-runtime.md +1 -1
- package/reference/autogen/packages/http-runtime.md +1 -1
- package/reference/autogen/packages/kernel.md +28 -1
- package/reference/autogen/packages/realtime.md +1 -1
- package/reference/autogen/packages/shell-web.md +1 -1
- package/reference/autogen/packages/storage-runtime.md +1 -1
- package/reference/autogen/packages/ui-generator.md +1 -1
- package/reference/autogen/packages/uploads-image-web.md +1 -1
- package/reference/autogen/packages/uploads-runtime.md +1 -1
- package/reference/autogen/packages/users-core.md +1 -1
- package/reference/autogen/packages/users-web.md +36 -1
- package/reference/autogen/packages/workspaces-core.md +5 -1
- package/reference/autogen/packages/workspaces-web.md +9 -1
- package/reference/autogen/tooling/config-eslint.md +1 -1
- package/reference/autogen/tooling/create-app.md +1 -1
- package/reference/autogen/tooling/jskit-catalog.md +1 -1
- package/reference/autogen/tooling/jskit-cli.md +1 -1
- package/reference/autogen/tooling/test-support.md +1 -1
- package/reference/autogen/tooling/testUtils.md +1 -1
- package/templates/app/AGENTS.md +2 -0
package/DISTR_AGENT.md
CHANGED
|
@@ -13,6 +13,7 @@ Use these references on demand:
|
|
|
13
13
|
|
|
14
14
|
- `reference/autogen/KERNEL_MAP.md`
|
|
15
15
|
- `reference/autogen/README.md`
|
|
16
|
+
- `patterns/INDEX.md`
|
|
16
17
|
- `guide/agent/index.md`
|
|
17
18
|
- `site/guide/index.md` when compressed guidance is ambiguous or missing nuance
|
|
18
19
|
- `templates/APP_BLUEPRINT.md`
|
|
@@ -28,6 +29,7 @@ Core rules:
|
|
|
28
29
|
- `Why this sticks: ...`
|
|
29
30
|
- `Not doing: ...`
|
|
30
31
|
- Keep that checkpoint compact. Do not expand it into a long preamble unless the developer asks for detail.
|
|
32
|
+
- When a request involves JSKIT UI, routing, surfaces, CRUDs, filters, placements, live actions, or similar implementation details, scan `patterns/INDEX.md` for matching keywords and read only the relevant pattern files.
|
|
31
33
|
- Reuse existing JSKIT helpers and runtime seams before adding new local helpers.
|
|
32
34
|
- A freshly scaffolded JSKIT app can still be in Stage 1. If platform decisions are not settled yet, continue with the initialize workflow before adding runtime packages.
|
|
33
35
|
- Do not treat a missing `config.tenancyMode` line or an untouched minimal scaffold as a final tenancy decision.
|
|
@@ -678,7 +678,7 @@ The baseline generator output is only the start. As the tutorial's `contacts`, `
|
|
|
678
678
|
|
|
679
679
|
- `src/server/listQueryValidators.js` when a list needs extra query filters beyond `q`
|
|
680
680
|
- `src/server/service.test.js` once save/delete rules stop being trivial
|
|
681
|
-
- `src/
|
|
681
|
+
- `packages/contacts/src/shared/contactListFilters.js` when the contacts CRUD gains structured list filters that both server and client should share
|
|
682
682
|
- `src/composables/addresses/useAddressDisplay.js` when addresses need app-specific display formatting
|
|
683
683
|
- `src/composables/comments/useCommentsListRuntime.js` when an embedded comments list needs local UI state
|
|
684
684
|
|
|
@@ -686,6 +686,7 @@ That is the right direction of growth:
|
|
|
686
686
|
|
|
687
687
|
- server customizations stay in the CRUD package
|
|
688
688
|
- presentation and page-specific UI state stay in app-owned client files
|
|
689
|
+
- shared structured list filters live best in a CRUD-package shared module that both server and client can import
|
|
689
690
|
|
|
690
691
|
## Search and filters: the deep dive
|
|
691
692
|
|
|
@@ -782,18 +783,64 @@ Usually nothing changes. The client can keep sending `q`.
|
|
|
782
783
|
- Do not dump every text column into search just because you can.
|
|
783
784
|
- In this tutorial CRUD, `notes` is a good example of a field you might leave out if you want fast, predictable list search.
|
|
784
785
|
|
|
785
|
-
### Pattern 3: structured
|
|
786
|
+
### Pattern 3: structured list filters from one shared definition
|
|
786
787
|
|
|
787
|
-
This is the
|
|
788
|
+
This is the default pattern once a CRUD list needs real filters.
|
|
788
789
|
|
|
789
|
-
|
|
790
|
+
Do not hand-build:
|
|
791
|
+
|
|
792
|
+
- one filter shape in the page
|
|
793
|
+
- a second filter shape in `listQueryValidators.js`
|
|
794
|
+
- and a third filter shape in `repository.js`
|
|
795
|
+
|
|
796
|
+
Instead:
|
|
797
|
+
|
|
798
|
+
1. define the filters once in a shared CRUD-package module
|
|
799
|
+
2. build the server runtime from that definition with `createCrudListFilters(...)`
|
|
800
|
+
3. choose the route/action invalid-value contract explicitly with `createQueryValidator({ invalidValues: "reject" | "discard" })`
|
|
801
|
+
4. build the client runtime from that same definition with `useCrudListFilters(...)`
|
|
802
|
+
|
|
803
|
+
For example, a shared filter-definition module can look like this:
|
|
804
|
+
|
|
805
|
+
```js
|
|
806
|
+
export const CONTACTS_LIST_FILTER_DEFINITIONS = Object.freeze({
|
|
807
|
+
onlyStaff: {
|
|
808
|
+
type: "flag",
|
|
809
|
+
label: "Staff"
|
|
810
|
+
},
|
|
811
|
+
onlyVip: {
|
|
812
|
+
type: "flag",
|
|
813
|
+
label: "VIP"
|
|
814
|
+
},
|
|
815
|
+
onlyArchived: {
|
|
816
|
+
type: "flag",
|
|
817
|
+
label: "Archived"
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
```
|
|
790
821
|
|
|
791
|
-
|
|
822
|
+
#### Exact file checklist
|
|
792
823
|
|
|
793
|
-
|
|
824
|
+
For a generated CRUD, treat this as the concrete file plan:
|
|
825
|
+
|
|
826
|
+
- create `packages/contacts/src/shared/contactListFilters.js`
|
|
827
|
+
- update `packages/contacts/src/server/registerRoutes.js` so the list route query validator includes an explicit `createQueryValidator({ invalidValues: ... })` choice, unless you already extracted list-query composition into `packages/contacts/src/server/listQueryValidators.js`
|
|
828
|
+
- update `packages/contacts/src/server/actions.js` so the list action input validator includes that same explicit contract choice
|
|
829
|
+
- update `packages/contacts/src/server/repository.js` so the list query path builds the `createCrudListFilters(...)` runtime and calls `applyQuery(...)`
|
|
830
|
+
- update the app-owned list page or list-runtime composable, usually under `src/pages/.../contacts/` or `src/composables/...`, so it builds `useCrudListFilters(...)`, passes `queryParams` into `useCrudList(...)`, and renders chips / reset behavior from that runtime
|
|
831
|
+
|
|
832
|
+
The only file in that list that is normally **new** is the shared module:
|
|
833
|
+
|
|
834
|
+
- `packages/contacts/src/shared/contactListFilters.js`
|
|
835
|
+
|
|
836
|
+
The others are normally edits to the generated CRUD package and the app-owned page layer that already exist.
|
|
837
|
+
|
|
838
|
+
#### Client side
|
|
839
|
+
|
|
840
|
+
Build the filter runtime directly from the shared definitions:
|
|
794
841
|
|
|
795
842
|
```js
|
|
796
|
-
const listFilters =
|
|
843
|
+
const listFilters = useCrudListFilters(CONTACTS_LIST_FILTER_DEFINITIONS);
|
|
797
844
|
|
|
798
845
|
const records = useCrudList({
|
|
799
846
|
resource: uiResource,
|
|
@@ -802,9 +849,7 @@ const records = useCrudList({
|
|
|
802
849
|
enabled: true,
|
|
803
850
|
mode: "query"
|
|
804
851
|
},
|
|
805
|
-
queryParams:
|
|
806
|
-
...listFilters.filters
|
|
807
|
-
})),
|
|
852
|
+
queryParams: listFilters.queryParams,
|
|
808
853
|
syncToRoute: {
|
|
809
854
|
enabled: true,
|
|
810
855
|
search: true,
|
|
@@ -814,65 +859,274 @@ const records = useCrudList({
|
|
|
814
859
|
});
|
|
815
860
|
```
|
|
816
861
|
|
|
817
|
-
|
|
862
|
+
That gives you, from one place:
|
|
863
|
+
|
|
864
|
+
- `listFilters.values`
|
|
865
|
+
- `listFilters.queryParams`
|
|
866
|
+
- `listFilters.presets`
|
|
867
|
+
- `listFilters.activeChips`
|
|
868
|
+
- `listFilters.hasActiveFilters`
|
|
869
|
+
- `listFilters.clearChip(...)`
|
|
870
|
+
- `listFilters.clearFilters()`
|
|
871
|
+
- `listFilters.toggle(...)` for flag filters
|
|
872
|
+
- `listFilters.applyPreset(...)`
|
|
873
|
+
- `listFilters.matchesPreset(...)`
|
|
874
|
+
|
|
875
|
+
So the same runtime owns:
|
|
876
|
+
|
|
877
|
+
- URL-synced query params
|
|
878
|
+
- filter chips
|
|
879
|
+
- reset logic
|
|
880
|
+
- preset application
|
|
881
|
+
- preset active-state matching
|
|
882
|
+
- small flag toggles
|
|
883
|
+
|
|
884
|
+
For relative-date quick filters, keep the date math in runtime presets instead of page-local helper state. `resolveValues(...)` runs at preset-apply time and receives `{ values, filters, presetKey, preset }`, so the preset can derive values from the current filter state and the normalized preset metadata:
|
|
885
|
+
|
|
886
|
+
```js
|
|
887
|
+
const listFilters = useCrudListFilters(
|
|
888
|
+
RECEIVAL_LIST_FILTER_DEFINITIONS,
|
|
889
|
+
{
|
|
890
|
+
presets: [
|
|
891
|
+
{
|
|
892
|
+
key: "today",
|
|
893
|
+
label: "Today",
|
|
894
|
+
resolveValues({ presetKey }) {
|
|
895
|
+
const today = formatDateInputValue(new Date());
|
|
896
|
+
return {
|
|
897
|
+
arrivalDate: {
|
|
898
|
+
from: today,
|
|
899
|
+
to: today
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
key: "last7",
|
|
906
|
+
label: "Last 7 Days",
|
|
907
|
+
resolveValues({ values }) {
|
|
908
|
+
const today = formatDateInputValue(new Date());
|
|
909
|
+
return {
|
|
910
|
+
arrivalDate: {
|
|
911
|
+
from: shiftDateInputValue(today, -6),
|
|
912
|
+
to: today
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
]
|
|
918
|
+
}
|
|
919
|
+
);
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
```vue
|
|
923
|
+
<v-chip
|
|
924
|
+
v-for="preset in listFilters.presets"
|
|
925
|
+
:key="preset.key"
|
|
926
|
+
:variant="listFilters.matchesPreset(preset.key) ? 'flat' : 'outlined'"
|
|
927
|
+
@click="listFilters.applyPreset(preset.key, { mode: 'merge' })"
|
|
928
|
+
>
|
|
929
|
+
{{ preset.label }}
|
|
930
|
+
</v-chip>
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
Use `mode: "merge"` when a preset should only change one filter group, such as the arrival date range, and should not clear the rest of the page's active filters.
|
|
818
934
|
|
|
819
|
-
|
|
820
|
-
- restorable on refresh
|
|
821
|
-
- preserved when opening a record and coming back
|
|
935
|
+
`matchesPreset(...)` is strict by design. It compares the preset against the full current filter state after basic normalization, and it does **not** silently drop extra `enumMany` or `recordIdMany` values that were hydrated from the route and still appear as chips. If the URL contains `status=archived&status=bogus`, a preset for only `archived` should render as inactive until the extra `bogus` value is cleared.
|
|
822
936
|
|
|
823
937
|
#### Server side
|
|
824
938
|
|
|
825
|
-
|
|
939
|
+
Build the server runtime from the same shared definitions:
|
|
826
940
|
|
|
827
941
|
```js
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
),
|
|
837
|
-
normalize(payload = {}) {
|
|
838
|
-
const normalized = {};
|
|
839
|
-
if (Object.hasOwn(payload, "hasEmail")) normalized.hasEmail = 1;
|
|
840
|
-
if (Object.hasOwn(payload, "hasPhone")) normalized.hasPhone = 1;
|
|
841
|
-
if (Object.hasOwn(payload, "hasNotes")) normalized.hasNotes = 1;
|
|
842
|
-
return normalized;
|
|
942
|
+
const contactsListFiltersRuntime = createCrudListFilters(
|
|
943
|
+
CONTACTS_LIST_FILTER_DEFINITIONS,
|
|
944
|
+
{
|
|
945
|
+
columns: {
|
|
946
|
+
onlyStaff: "is_staff",
|
|
947
|
+
onlyVip: "vip",
|
|
948
|
+
onlyArchived: "archived"
|
|
949
|
+
}
|
|
843
950
|
}
|
|
951
|
+
);
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
There is no default query-validator mode and no `runtime.queryValidator` alias. Create the validator that matches the contract you want at that route or action boundary.
|
|
955
|
+
|
|
956
|
+
Strict contract example:
|
|
957
|
+
|
|
958
|
+
```js
|
|
959
|
+
const contactsListFiltersQueryValidator = contactsListFiltersRuntime.createQueryValidator({
|
|
960
|
+
invalidValues: "reject" // malformed filter values should fail validation and return 400
|
|
961
|
+
});
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
Lenient contract example:
|
|
965
|
+
|
|
966
|
+
```js
|
|
967
|
+
const contactsListFiltersQueryValidator = contactsListFiltersRuntime.createQueryValidator({
|
|
968
|
+
invalidValues: "discard" // malformed filter values should be ignored and dropped by normalize()
|
|
844
969
|
});
|
|
845
970
|
```
|
|
846
971
|
|
|
847
|
-
Wire
|
|
972
|
+
Wire the runtime into the list validator and the repository:
|
|
973
|
+
|
|
974
|
+
```js
|
|
975
|
+
queryValidator: [
|
|
976
|
+
listCursorPaginationQueryValidator,
|
|
977
|
+
listSearchQueryValidator,
|
|
978
|
+
contactsListFiltersQueryValidator,
|
|
979
|
+
listParentFilterQueryValidator,
|
|
980
|
+
lookupIncludeQueryValidator
|
|
981
|
+
]
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
Use that same `contactsListFiltersQueryValidator` anywhere else the list query is validated, such as the list action input validator if your CRUD package validates query shape at both the route and action boundaries.
|
|
985
|
+
|
|
986
|
+
Choose the invalid-value contract deliberately:
|
|
987
|
+
|
|
988
|
+
- there is no default mode and no fallback alias, so every route or action that validates structured filters must call `createQueryValidator({ invalidValues: ... })` explicitly
|
|
989
|
+
- use `invalidValues: "reject"` when malformed filter values should fail validation and produce a 400-style contract error
|
|
990
|
+
- use `invalidValues: "discard"` when malformed filter values should be ignored and normalization should drop them
|
|
991
|
+
- route query validation runs before auth, so this choice changes whether malformed unauthenticated requests fail at validation or fall through to auth
|
|
992
|
+
- for normal HTTP CRUD handlers, route-level `discard` means the handler receives already-normalized query input, so the action layer will not see those discarded bad values again later
|
|
993
|
+
- `jskit doctor` flags `createQueryValidator(...)` calls that do not spell out `invalidValues` directly at the call site
|
|
848
994
|
|
|
849
995
|
```js
|
|
850
996
|
async function list(query = {}, callOptions = {}) {
|
|
851
997
|
return crudRepositoryList(repositoryRuntime, knex, query, options, callOptions, {
|
|
852
998
|
modifyQuery(dbQuery, context = {}) {
|
|
853
|
-
|
|
999
|
+
return contactsListFiltersRuntime.applyQuery(dbQuery, context.query || {});
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
```
|
|
854
1004
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1005
|
+
#### Best practices
|
|
1006
|
+
|
|
1007
|
+
- Put the filter definitions in the CRUD package, not the page. Both server and client need them.
|
|
1008
|
+
- Keep the filter keys identical all the way through: definition key, query param key, and repository meaning.
|
|
1009
|
+
- Do not expect a default `runtime.queryValidator` to exist. Every structured-filter validator must be created explicitly with `createQueryValidator({ invalidValues: ... })`.
|
|
1010
|
+
- Use `type: "presence"` for null/not-null filters such as assigned vs unassigned storage. Do not model those as custom enums plus `applyQuery(...)` overrides unless the SQL semantics are genuinely different from `whereNotNull(...)` / `whereNull(...)`.
|
|
1011
|
+
- Use `createCrudListFilters(...)` unless the list semantics are truly unusual.
|
|
1012
|
+
- Use `q` for free-text and explicit query params for structured filters.
|
|
1013
|
+
- Run `jskit doctor` after wiring filters. It flags inline/local filter-definition objects passed into `useCrudListFilters(...)` or `createCrudListFilters(...)`, and it flags `createQueryValidator(...)` calls that hide the invalid-value policy.
|
|
1014
|
+
|
|
1015
|
+
### Pattern 4: lookup-backed structured filters
|
|
1016
|
+
|
|
1017
|
+
This is the next real-world step: filters like `supplierContactId`, `locationId`, or `contactId` where the user needs:
|
|
1018
|
+
|
|
1019
|
+
- remote autocomplete search
|
|
1020
|
+
- URL-synced selected ids
|
|
1021
|
+
- readable chip labels instead of raw ids
|
|
1022
|
+
|
|
1023
|
+
The right pattern is:
|
|
1024
|
+
|
|
1025
|
+
1. keep the lookup filter in the shared definitions
|
|
1026
|
+
2. use `useCrudListFilters(...)` for state, chips, and query params
|
|
1027
|
+
3. use `useCrudListFilterLookups(...)` for option loading and label resolution
|
|
1028
|
+
|
|
1029
|
+
Example shared definition:
|
|
1030
|
+
|
|
1031
|
+
```js
|
|
1032
|
+
export const RECEIVAL_LIST_FILTER_DEFINITIONS = Object.freeze({
|
|
1033
|
+
supplierContactId: {
|
|
1034
|
+
type: "recordIdMany",
|
|
1035
|
+
label: "Supplier",
|
|
1036
|
+
lookup: {
|
|
1037
|
+
namespace: "contacts"
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
pollenTypeId: {
|
|
1041
|
+
type: "recordIdMany",
|
|
1042
|
+
label: "Pollen Type",
|
|
1043
|
+
lookup: {
|
|
1044
|
+
namespace: "pollen-types",
|
|
1045
|
+
labelKey: "name"
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
#### Exact file checklist
|
|
1052
|
+
|
|
1053
|
+
Lookup-backed filters do **not** change the ownership model from Pattern 3. The file plan is still:
|
|
1054
|
+
|
|
1055
|
+
- keep the shared definition in `packages/receivals/src/shared/receivalListFilters.js`
|
|
1056
|
+
- update the same server validator and repository files from Pattern 3
|
|
1057
|
+
- update the app-owned list page or list-runtime composable so it creates both `useCrudListFilters(...)` and `useCrudListFilterLookups(...)`
|
|
1058
|
+
- bind the lookup UI, such as `v-autocomplete`, from `filterLookups.resolveLookup(...)`
|
|
1059
|
+
|
|
1060
|
+
Do **not** create a second page-local filter schema just because the UI needs remote autocomplete. The shared definition file stays the source of truth.
|
|
1061
|
+
|
|
1062
|
+
#### Client side
|
|
1063
|
+
|
|
1064
|
+
```js
|
|
1065
|
+
let filterLookups = null;
|
|
1066
|
+
|
|
1067
|
+
const listFilters = useCrudListFilters(
|
|
1068
|
+
RECEIVAL_LIST_FILTER_DEFINITIONS,
|
|
1069
|
+
{
|
|
1070
|
+
labelResolvers: {
|
|
1071
|
+
supplierContactId(value) {
|
|
1072
|
+
return filterLookups?.resolveLookupLabel("supplierContactId", value, "Supplier") || "";
|
|
860
1073
|
}
|
|
861
|
-
|
|
862
|
-
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
filterLookups = useCrudListFilterLookups(
|
|
1079
|
+
RECEIVAL_LIST_FILTER_DEFINITIONS,
|
|
1080
|
+
{
|
|
1081
|
+
values: listFilters.values,
|
|
1082
|
+
queryKeyPrefix: ["ui-generator", "receivals", "filters"],
|
|
1083
|
+
placementSourcePrefix: "ui-generator.receivals.list.filters",
|
|
1084
|
+
requestQueryParams: {
|
|
1085
|
+
supplierContactId: { limit: 25 }
|
|
1086
|
+
},
|
|
1087
|
+
labelResolvers: {
|
|
1088
|
+
supplierContactId(item = {}) {
|
|
1089
|
+
return `${item.firstName} ${item.lastName}`.trim();
|
|
863
1090
|
}
|
|
864
1091
|
}
|
|
865
|
-
}
|
|
866
|
-
|
|
1092
|
+
}
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
const supplierFilterLookup = filterLookups.resolveLookup("supplierContactId");
|
|
867
1096
|
```
|
|
868
1097
|
|
|
1098
|
+
Then bind the autocomplete:
|
|
1099
|
+
|
|
1100
|
+
```vue
|
|
1101
|
+
<v-autocomplete
|
|
1102
|
+
v-model="listFilters.values.supplierContactId"
|
|
1103
|
+
:items="supplierFilterLookup.options"
|
|
1104
|
+
:search="supplierFilterLookup.searchQuery"
|
|
1105
|
+
:loading="supplierFilterLookup.isLoading"
|
|
1106
|
+
item-title="label"
|
|
1107
|
+
item-value="value"
|
|
1108
|
+
multiple
|
|
1109
|
+
chips
|
|
1110
|
+
no-filter
|
|
1111
|
+
@update:search="supplierFilterLookup.setSearch"
|
|
1112
|
+
/>
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
#### Why this is better than a page-local `useList()` wrapper
|
|
1116
|
+
|
|
1117
|
+
- the CRUD filter state still lives in `useCrudListFilters(...)`
|
|
1118
|
+
- the autocomplete loading logic lives in one reusable helper
|
|
1119
|
+
- the label resolution used by filter chips and the autocomplete stays consistent
|
|
1120
|
+
- a second screen can reuse the same pattern instead of rewriting it
|
|
1121
|
+
|
|
869
1122
|
#### Best practices
|
|
870
1123
|
|
|
871
|
-
-
|
|
872
|
-
- Use
|
|
873
|
-
-
|
|
1124
|
+
- Put lookup metadata in the shared filter definitions.
|
|
1125
|
+
- Use `useCrudListFilterLookups(...)` for remote filter autocompletes instead of building a custom `useList()` wrapper per screen.
|
|
1126
|
+
- Keep lookup label formatting on the client side. It is UI presentation, not repository logic.
|
|
1127
|
+
- Keep unusual SQL semantics, such as `pending = whereNull(...)`, in the server runtime `apply` override.
|
|
874
1128
|
|
|
875
|
-
### Pattern
|
|
1129
|
+
### Pattern 5: free-text search plus structured filters together
|
|
876
1130
|
|
|
877
1131
|
This is the most common real-world CRUD list.
|
|
878
1132
|
|
|
@@ -895,7 +1149,7 @@ Let the generic list search handle `q`, and let your custom validator/repository
|
|
|
895
1149
|
- Preserve the current route query when linking to view/edit pages so users can return to the same filtered list state.
|
|
896
1150
|
- Let list changes reset pagination; `useCrudList()` already does this for search and query-param changes.
|
|
897
1151
|
|
|
898
|
-
### Pattern
|
|
1152
|
+
### Pattern 6: parent-scoped child CRUD search for `addresses`
|
|
899
1153
|
|
|
900
1154
|
For nested CRUDs such as the `addresses` resource from the previous chapter, parent scoping and search usually work together.
|
|
901
1155
|
|
|
@@ -935,7 +1189,7 @@ That keeps child-list filtering grounded in the actual resource definition inste
|
|
|
935
1189
|
- Let the resource contract define parent filter shape.
|
|
936
1190
|
- Treat parent-scoped filtering as repository/query behavior, not as presentation logic.
|
|
937
1191
|
|
|
938
|
-
### Pattern
|
|
1192
|
+
### Pattern 7: local-only search for embedded `comments`
|
|
939
1193
|
|
|
940
1194
|
Sometimes server search is unnecessary.
|
|
941
1195
|
|
|
@@ -1038,10 +1292,12 @@ Use `scaffold-field` when it fits, then review the generated result.
|
|
|
1038
1292
|
|
|
1039
1293
|
Touch:
|
|
1040
1294
|
|
|
1041
|
-
-
|
|
1042
|
-
- `
|
|
1043
|
-
-
|
|
1044
|
-
- `
|
|
1295
|
+
- `packages/<crud>/src/shared/<crud>ListFilters.js` and make it the only authored filter-definition module
|
|
1296
|
+
- `packages/<crud>/src/server/registerRoutes.js` and `packages/<crud>/src/server/actions.js` so the list validator includes an explicit `createQueryValidator({ invalidValues: ... })` choice, or `packages/<crud>/src/server/listQueryValidators.js` if you extracted list-query composition there
|
|
1297
|
+
- `packages/<crud>/src/server/repository.js` so the list query applies the runtime
|
|
1298
|
+
- the app-owned list page or list-runtime composable that calls `useCrudList(...)`
|
|
1299
|
+
|
|
1300
|
+
If the filter is lookup-backed, touch that same client file again to wire `useCrudListFilterLookups(...)`.
|
|
1045
1301
|
|
|
1046
1302
|
### "I want a new save rule."
|
|
1047
1303
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/agent-docs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Distributed JSKIT agent workflows, guides, and generated reference maps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"DISTR_AGENT.md",
|
|
8
8
|
"guide",
|
|
9
|
+
"patterns",
|
|
9
10
|
"reference",
|
|
10
11
|
"skills",
|
|
11
12
|
"templates",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# JSKIT Patterns Index
|
|
2
|
+
|
|
3
|
+
Use this index as the first stop for recurring JSKIT implementation heuristics and workflow traps.
|
|
4
|
+
|
|
5
|
+
How to use it:
|
|
6
|
+
|
|
7
|
+
- Match the user request against the keywords below.
|
|
8
|
+
- Open only the relevant pattern files.
|
|
9
|
+
- Use patterns as implementation guidance, not as permission to skip normal scoping or user clarification.
|
|
10
|
+
|
|
11
|
+
## Keyword Map
|
|
12
|
+
|
|
13
|
+
- tabs, menu items, icons, shell links, profile links, subpage tabs, placements
|
|
14
|
+
- `placements.md`
|
|
15
|
+
- surfaces, app/admin/home/console, "which surface", route ownership, placement visibility
|
|
16
|
+
- `surfaces.md`
|
|
17
|
+
- child crud, nested crud, embedded list, subroute, separate page, parent/child layout
|
|
18
|
+
- `child-cruds.md`
|
|
19
|
+
- CRUD links, record placeholders, `paths.page()`, `resolveViewUrl`, `resolveEditUrl`, `resolveParams`
|
|
20
|
+
- `crud-links.md`
|
|
21
|
+
- live actions, checkbox, toggle, patch button, inline action, `useCommand()`
|
|
22
|
+
- `live-actions.md`
|
|
23
|
+
- filter, filters, search facets, chips, date range, enum filter, lookup filter, `useCrudListFilters`, `createCrudListFilters`
|
|
24
|
+
- `filters.md`
|
|
25
|
+
|
|
26
|
+
## Current Patterns
|
|
27
|
+
|
|
28
|
+
- [placements.md](./placements.md)
|
|
29
|
+
- [surfaces.md](./surfaces.md)
|
|
30
|
+
- [child-cruds.md](./child-cruds.md)
|
|
31
|
+
- [crud-links.md](./crud-links.md)
|
|
32
|
+
- [live-actions.md](./live-actions.md)
|
|
33
|
+
- [filters.md](./filters.md)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Child CRUD Layout Patterns
|
|
2
|
+
|
|
3
|
+
Use when:
|
|
4
|
+
|
|
5
|
+
- planning nested CRUDs
|
|
6
|
+
- adding records under a parent record
|
|
7
|
+
- deciding whether child records need their own page or stay inside the parent view
|
|
8
|
+
|
|
9
|
+
Rules:
|
|
10
|
+
|
|
11
|
+
- Before generating a child CRUD, ask how the user wants the child records laid out.
|
|
12
|
+
- Do not assume one layout pattern by default.
|
|
13
|
+
|
|
14
|
+
Clarify these options:
|
|
15
|
+
|
|
16
|
+
- embedded in the parent view
|
|
17
|
+
- often a list rendered directly inside the parent page
|
|
18
|
+
- embedded as child subroutes
|
|
19
|
+
- the parent page becomes a routed container or subpage host
|
|
20
|
+
- totally separate route/page
|
|
21
|
+
- the child list/view/edit flow lives in its own route area
|
|
22
|
+
|
|
23
|
+
Why this matters:
|
|
24
|
+
|
|
25
|
+
- the answer changes route structure, placements, host containers, and which generator flow fits best
|
|
26
|
+
- child CRUD layout mistakes are expensive to unwind later
|
|
27
|
+
|
|
28
|
+
Avoid:
|
|
29
|
+
|
|
30
|
+
- generating nested CRUD routes before the parent/child layout is agreed
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# CRUD Link Resolution Patterns
|
|
2
|
+
|
|
3
|
+
Use when:
|
|
4
|
+
|
|
5
|
+
- customizing generated CRUD pages
|
|
6
|
+
- wiring buttons or links on list/view/edit pages
|
|
7
|
+
- building nested CRUD links
|
|
8
|
+
|
|
9
|
+
Rules:
|
|
10
|
+
|
|
11
|
+
- Use `paths.page()` for surface-aware navigation that only needs surface params.
|
|
12
|
+
- Do not use `paths.page()` with CRUD record placeholders such as `:contactId`, `:addressId`, `:todoListId`, or `:todoItemId` in CRUD-bound route suffixes.
|
|
13
|
+
- For CRUD-bound links, use the runtime that owns the current CRUD scope.
|
|
14
|
+
|
|
15
|
+
Prefer:
|
|
16
|
+
|
|
17
|
+
- list pages
|
|
18
|
+
- `records.resolveViewUrl(record)`
|
|
19
|
+
- `records.resolveEditUrl(record)`
|
|
20
|
+
- `records.resolveParams(template, extraParams)`
|
|
21
|
+
- view pages
|
|
22
|
+
- `view.listUrl`
|
|
23
|
+
- `view.editUrl`
|
|
24
|
+
- `view.resolveParams(template, extraParams)`
|
|
25
|
+
- add/edit pages
|
|
26
|
+
- `formRuntime.addEdit.resolveParams(template, extraParams)`
|
|
27
|
+
|
|
28
|
+
Scope rule:
|
|
29
|
+
|
|
30
|
+
- Use the runtime anchored to the record that owns the action.
|
|
31
|
+
- On a parent record view with nested child routes, parent actions should still resolve from the parent `view` runtime even while a child route is active.
|
|
32
|
+
|
|
33
|
+
Avoid:
|
|
34
|
+
|
|
35
|
+
- `paths.page("/lists/:todoListId/items/new")`
|
|
36
|
+
- `paths.page("/lists/:todoListId/edit")`
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Structured Filters
|
|
2
|
+
|
|
3
|
+
Use when:
|
|
4
|
+
- a CRUD list needs flags, enums, date ranges, record-id filters, or lookup-backed filters
|
|
5
|
+
- the user asks to "add a filter", "make the list filterable", or "keep filters in the URL"
|
|
6
|
+
- a screen mixes free-text search with structured facets
|
|
7
|
+
|
|
8
|
+
Ask first:
|
|
9
|
+
- which fields are filterable
|
|
10
|
+
- whether each filter is a flag, enum, multi-enum, date/date-range, number-range, record id, or lookup-backed record id
|
|
11
|
+
- whether filters must sync to the route query
|
|
12
|
+
- whether the screen needs chips and clear/reset behavior
|
|
13
|
+
- whether lookup filters need remote autocomplete search
|
|
14
|
+
- whether there are presets such as "Today", "Last 7 Days", or "Only Archived"
|
|
15
|
+
|
|
16
|
+
Default JSKIT pattern:
|
|
17
|
+
1. Put shared filter definitions in the CRUD package.
|
|
18
|
+
Example path: `packages/<crud>/src/shared/<crud>ListFilters.js`
|
|
19
|
+
2. Build the server runtime from that module with `createCrudListFilters(...)`.
|
|
20
|
+
3. Build route/action validators explicitly with `createQueryValidator({ invalidValues: "reject" | "discard" })`, and use `applyQuery(...)` in the repository. There is no default validator mode and no `runtime.queryValidator` alias.
|
|
21
|
+
4. Build the client runtime from the same shared definitions with `useCrudListFilters(...)`. Presets can use static `values` or dynamic `resolveValues({ values, filters, presetKey, preset })`.
|
|
22
|
+
5. Pass `listFilters.queryParams` into `useCrudList(...)`.
|
|
23
|
+
6. For lookup-backed filters, use `useCrudListFilterLookups(...)` instead of hand-rolling `useList()` in each page.
|
|
24
|
+
|
|
25
|
+
Exact file checklist:
|
|
26
|
+
- create `packages/<crud>/src/shared/<crud>ListFilters.js`
|
|
27
|
+
- update `packages/<crud>/src/server/registerRoutes.js` and `packages/<crud>/src/server/actions.js` so the list query validator includes an explicit `createQueryValidator({ invalidValues: ... })` choice, or update `packages/<crud>/src/server/listQueryValidators.js` if that package already extracts list-query composition there
|
|
28
|
+
- update `packages/<crud>/src/server/repository.js` so list queries call the runtime's `applyQuery(...)`
|
|
29
|
+
- update the app-owned list page or list-runtime composable under `src/pages/...` or `src/composables/...` so it builds `useCrudListFilters(...)`, passes `queryParams` into `useCrudList(...)`, and binds chips / reset behavior
|
|
30
|
+
- for lookup-backed filters, update that same client file to build `useCrudListFilterLookups(...)` and bind the lookup control from `resolveLookup(...)`
|
|
31
|
+
|
|
32
|
+
Validation mode is part of the contract:
|
|
33
|
+
- there is no default mode and no fallback alias, so every route/action boundary that validates structured filters must call `createQueryValidator({ invalidValues: ... })` explicitly
|
|
34
|
+
- use `invalidValues: "reject"` when malformed filter values should fail validation and return a 400-style contract error
|
|
35
|
+
- use `invalidValues: "discard"` when malformed filter values should be ignored and normalization should drop them
|
|
36
|
+
- route query validation runs before auth, so this choice affects whether malformed unauthenticated requests fail at validation or fall through to auth
|
|
37
|
+
- for normal HTTP CRUD handlers, route-level `discard` means the action layer receives already-normalized query input; do not assume route `discard` plus action `reject` will still reject malformed HTTP query strings later
|
|
38
|
+
- `jskit doctor` flags `createQueryValidator(...)` calls that do not spell out `invalidValues` directly at the call site
|
|
39
|
+
|
|
40
|
+
Keep separate:
|
|
41
|
+
- free-text search uses `records.searchQuery` and `q`
|
|
42
|
+
- structured filters use explicit query params through `useCrudListFilters(...)`
|
|
43
|
+
|
|
44
|
+
Use `useCrudListFilterLookups(...)` when:
|
|
45
|
+
- a filter is `recordId` or `recordIdMany`
|
|
46
|
+
- the UI needs remote autocomplete search
|
|
47
|
+
- chips should show readable labels instead of raw ids
|
|
48
|
+
|
|
49
|
+
Use built-in `presence` when:
|
|
50
|
+
- a filter means null vs not-null, such as assigned vs unassigned storage
|
|
51
|
+
- the UI wants custom labels like "Assigned" / "Unassigned" but the transport contract can stay `present` / `missing`
|
|
52
|
+
- you do **not** need custom SQL beyond `whereNotNull(...)` / `whereNull(...)`
|
|
53
|
+
|
|
54
|
+
Use runtime presets when:
|
|
55
|
+
- the page has quick filters such as "Today", "Last 7 Days", or "Only Archived"
|
|
56
|
+
- the preset should reuse the same filter state/reset/query-param runtime as the rest of the page
|
|
57
|
+
- relative-date presets need dynamic `resolveValues()` instead of hard-coded dates
|
|
58
|
+
- the active-state UI should reflect the full current filter state through `matchesPreset(...)`, including extra route-hydrated values that still appear as chips
|
|
59
|
+
|
|
60
|
+
Put unusual SQL semantics on the server:
|
|
61
|
+
- examples: `pending` meaning `whereNull("ccp1_passed")`, or business-specific status buckets that combine multiple columns
|
|
62
|
+
- implement those in `createCrudListFilters(..., { apply: { ... } })`
|
|
63
|
+
- do not use custom `apply` just for null/not-null checks when `type: "presence"` fits
|
|
64
|
+
|
|
65
|
+
Avoid:
|
|
66
|
+
- local filter composables that duplicate the same keys the server already knows about
|
|
67
|
+
- a custom validator shape that does not match the page state
|
|
68
|
+
- assuming a default `runtime.queryValidator` exists; always create the validator explicitly with `createQueryValidator({ invalidValues: ... })`
|
|
69
|
+
- hand-rolled preset apply/reset/active-state helpers when `useCrudListFilters(..., { presets })`, `applyPreset(...)`, and `matchesPreset(...)` fit
|
|
70
|
+
- per-screen `useList()` wrappers for lookup-backed filters when `useCrudListFilterLookups(...)` fits
|
|
71
|
+
- a second page-local filter-definition file when `packages/<crud>/src/shared/<crud>ListFilters.js` should be the source of truth
|
|
72
|
+
- overloading `q` with structured filter meaning
|
|
73
|
+
- local or inline filter-definition objects passed into `useCrudListFilters(...)` or `createCrudListFilters(...)`; `jskit doctor` expects those runtimes to import from a CRUD shared `*ListFilters` module
|
|
74
|
+
|
|
75
|
+
Good shape:
|
|
76
|
+
- `packages/receivals/src/shared/receivalListFilters.js`
|
|
77
|
+
- `createCrudListFilters(RECEIVAL_LIST_FILTER_DEFINITIONS, ...)`
|
|
78
|
+
- `const listFilters = useCrudListFilters(RECEIVAL_LIST_FILTER_DEFINITIONS, { presets: [...] })`
|
|
79
|
+
- `const filterLookups = useCrudListFilterLookups(RECEIVAL_LIST_FILTER_DEFINITIONS, { values: listFilters.values, ... })`
|
|
80
|
+
- `queryParams: listFilters.queryParams`
|
|
81
|
+
|
|
82
|
+
Preset contract notes:
|
|
83
|
+
- `resolveValues({ values, filters, presetKey, preset })` receives the current filter state and the normalized preset metadata, so relative-date presets can derive values at apply time without page-local helper state
|
|
84
|
+
- `matchesPreset(...)` compares against the full current filter state after basic normalization; it does **not** silently drop extra `enumMany` or `recordIdMany` values that were hydrated from the route and still show up as active chips
|
|
85
|
+
- if the URL contains `status=archived&status=bogus`, a preset for only `archived` should not render as active while the `bogus` chip is still visible
|
|
86
|
+
|
|
87
|
+
Review checks:
|
|
88
|
+
- one shared filter definition source of truth
|
|
89
|
+
- server validator/repository logic derived from that source
|
|
90
|
+
- client query params/chips/reset logic derived from that source
|
|
91
|
+
- lookup-backed filters use the shared lookup helper, not a page-local mini-framework
|
|
92
|
+
- `jskit doctor` stays clean for filter ownership and explicit validator policy
|