Skip to content

Commit 9d0ffd0

Browse files
Close #7 case editor v1
1 parent cbfdd73 commit 9d0ffd0

File tree

10 files changed

+292
-136
lines changed

10 files changed

+292
-136
lines changed

packages/gui/src/components/case-page.ts

+62-18
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,86 @@
11
import m from 'mithril';
2-
import { Pages } from '../models';
3-
import { MeiosisComponent, t } from '../services';
4-
import { Chips } from 'mithril-materialized';
5-
import { ChipData } from 'materialize-css';
2+
import { CrimeScriptFilter, Pages } from '../models';
3+
import { I18N, MeiosisComponent, routingSvc, t } from '../services';
4+
import { TextInput } from 'mithril-materialized';
5+
import { FormAttributes, LayoutForm, UIForm } from 'mithril-ui-form';
6+
import { attributeFilterFormFactory, crimeScriptFilterFormFactory } from '../models/forms';
67

78
export const CasePage: MeiosisComponent = () => {
8-
const tags: string[] = [];
9+
let crimeScriptFilterForm: UIForm<CrimeScriptFilter>;
910

1011
return {
1112
oninit: ({
1213
attrs: {
14+
state: { model },
1315
actions: { setPage },
1416
},
1517
}) => {
18+
const { products = [], geoLocations = [], locations = [], cast = [], attributes = [], transports = [] } = model;
19+
crimeScriptFilterForm = [
20+
...crimeScriptFilterFormFactory(products, locations, geoLocations, 'search'),
21+
...attributeFilterFormFactory(cast, attributes, transports, 'search'),
22+
] as UIForm<CrimeScriptFilter>;
1623
setPage(Pages.CASE);
1724
},
1825
view: ({ attrs: { state, actions } }) => {
19-
const { caseTags = [], caseResults = [] } = state;
26+
const { caseResults = [], caseFilter, crimeScriptFilter = {} as CrimeScriptFilter, model } = state;
2027
const { update } = actions;
21-
const data: ChipData[] = caseTags.map((tag) => ({ tag }));
2228

23-
return m('#case-page.row.case.page.markdown', [
29+
return m('#case-page.row.case.page', [
30+
m(LayoutForm, {
31+
form: crimeScriptFilterForm,
32+
obj: crimeScriptFilter,
33+
onchange: () => {
34+
actions.update({ crimeScriptFilter });
35+
},
36+
i18n: I18N,
37+
} as FormAttributes<CrimeScriptFilter>),
2438
m('.col.s12', [
25-
m(Chips, {
26-
data,
39+
m(TextInput, {
2740
label: 'Aangetroffen zaken',
41+
iconName: 'search',
2842
className: 'center-align',
29-
onchange: (tags) => {
30-
const caseTags = tags.map((tag) => tag.tag);
31-
update({ caseTags });
43+
initialValue: caseFilter,
44+
onchange: (v) => {
45+
// const caseTags = tags.map((tag) => tag.tag);
46+
update({ caseFilter: v });
3247
},
3348
}),
3449
]),
35-
// caseResults.length > 0 &&
36-
caseTags.length > 0 && m('.col.s12', m('h4', 'Meest waarschijnlijke acts')),
37-
caseTags.map((tag) => {
38-
return m('.col.s12', m('h5', tag));
39-
}),
50+
caseResults &&
51+
m('.col.s12', [
52+
m('p', t('HITS', caseResults.length)),
53+
caseResults.length > 0 && [
54+
m(
55+
'ol',
56+
caseResults.map(({ crimeScriptIdx, totalScore, acts }) =>
57+
m(
58+
'li',
59+
`${model.crimeScripts[crimeScriptIdx].label} (score ${totalScore})`,
60+
m(
61+
'ul.browser-default',
62+
acts.map(({ actIdx, phaseIdx, score }) =>
63+
m(
64+
'li',
65+
m(
66+
'a.truncate',
67+
{
68+
style: { cursor: 'pointer' },
69+
href: routingSvc.href(Pages.CRIME_SCRIPT, `id=${model.crimeScripts[crimeScriptIdx].id}`),
70+
onclick: () => {
71+
actions.setLocation(model.crimeScripts[crimeScriptIdx].id, actIdx, phaseIdx);
72+
},
73+
},
74+
`${actIdx >= 0 ? model.acts[actIdx].label : t('TEXT')} (score: ${score})`
75+
)
76+
)
77+
)
78+
)
79+
)
80+
)
81+
),
82+
],
83+
]),
4084
]);
4185
},
4286
};

packages/gui/src/components/home-page.ts

+13-31
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import m from 'mithril';
2-
import { Act, CrimeScript, Hierarchical, ID, Labeled, Pages, scriptIcon } from '../models';
3-
import { CrimeScriptFilter, MeiosisComponent, routingSvc } from '../services';
2+
import { Act, CrimeScript, CrimeScriptFilter, Hierarchical, ID, Labeled, Pages, scriptIcon } from '../models';
3+
import { MeiosisComponent, routingSvc } from '../services';
44
import { FlatButton, uniqueId, Icon } from 'mithril-materialized';
55
import { I18N, t } from '../services/translations';
6-
import { toCommaSeparatedList, toOptions } from '../utils';
6+
import { toCommaSeparatedList } from '../utils';
77
import { FormAttributes, LayoutForm, UIForm } from 'mithril-ui-form';
8+
import { crimeScriptFilterFormFactory } from '../models/forms';
89

910
export const HomePage: MeiosisComponent = () => {
1011
const actLocations = (cs: CrimeScript, acts: Act[]) => {
@@ -35,12 +36,21 @@ export const HomePage: MeiosisComponent = () => {
3536
return [...included, ...children, ...grandchildren];
3637
};
3738

39+
let crimeScriptFilterForm: UIForm<CrimeScriptFilter>;
40+
3841
return {
3942
oninit: ({
4043
attrs: {
44+
state: { model },
4145
actions: { setPage },
4246
},
4347
}) => {
48+
const { products = [], geoLocations = [], locations = [] } = model;
49+
crimeScriptFilterForm = crimeScriptFilterFormFactory(
50+
products,
51+
locations,
52+
geoLocations
53+
) as UIForm<CrimeScriptFilter>;
4454
setPage(Pages.HOME);
4555
},
4656
view: ({ attrs: { state, actions } }) => {
@@ -65,34 +75,6 @@ export const HomePage: MeiosisComponent = () => {
6575
}
6676
: (_cs: CrimeScript, _idx: number, _arr: CrimeScript[]) => true;
6777

68-
const crimeScriptFilterForm = [
69-
{
70-
id: 'productIds',
71-
label: t('PRODUCTS', 2),
72-
icon: 'filter_alt',
73-
type: 'select',
74-
multiple: true,
75-
options: toOptions(products),
76-
className: 'col s4',
77-
},
78-
{
79-
id: 'locationIds',
80-
label: t('LOCATIONS', 2),
81-
type: 'select',
82-
multiple: true,
83-
options: toOptions(locations),
84-
className: 'col s4',
85-
},
86-
{
87-
id: 'geoLocationIds',
88-
label: t('GEOLOCATIONS', 2),
89-
type: 'select',
90-
multiple: true,
91-
options: toOptions(geoLocations),
92-
className: 'col s4',
93-
},
94-
] as UIForm<CrimeScriptFilter>;
95-
9678
return m('#home-page.row.home.page', [
9779
isAdmin &&
9880
m(

packages/gui/src/components/settings-page.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import m, { FactoryComponent } from 'mithril';
2-
import { CrimeScript, ID, Pages, Act, Hierarchical, Labeled, DataModel } from '../models';
2+
import {
3+
CrimeScript,
4+
ID,
5+
Pages,
6+
Act,
7+
Hierarchical,
8+
Labeled,
9+
DataModel,
10+
FlexSearchResult,
11+
SearchScore,
12+
} from '../models';
313
import { MeiosisComponent, routingSvc, t } from '../services';
414
import { deepCopy, FormAttributes, LayoutForm } from 'mithril-ui-form';
515
import { Collapsible, FlatButton, Tabs } from 'mithril-materialized';
616
import { attrForm, AttributeType } from '../models/forms';
717
import { TextInputWithClear } from './ui/text-input-with-clear';
8-
import { FlexSearchResult, SearchScore } from '../services/flex-search';
918

1019
type ItemType = 'cast' | 'attribute' | 'location' | 'geolocation' | 'transport' | 'product';
1120

packages/gui/src/models/data-model.ts

+17
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ export type SearchResult = {
6161
}[];
6262
};
6363

64+
export enum SearchScore {
65+
EXACT_MATCH = 3,
66+
PARENT_MATCH = 2,
67+
OTHER_MATCH = 1,
68+
}
69+
70+
export type FlexSearchResult = [crimeScriptIdx: number, actIdx: number, phaseIdx: number, score: number];
71+
72+
export type CrimeScriptFilter = {
73+
productIds: ID[];
74+
geoLocationIds: ID[];
75+
locationIds: ID[];
76+
roleIds: ID[];
77+
attributeIds: ID[];
78+
transportIds: ID[];
79+
};
80+
6481
export type ID = string;
6582

6683
export type Labeled = {

packages/gui/src/models/forms.ts

+70-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { UIForm } from 'mithril-ui-form';
2-
import { CrimeScript, Literature, Labeled, Hierarchical } from './data-model';
2+
import { CrimeScript, Literature, Labeled, Hierarchical, CrimeScriptFilter } from './data-model';
3+
import { toOptions } from '../utils';
34
import { t } from '../services';
45

56
export type AttributeType = 'cast' | 'attributes' | 'transports' | 'locations' | 'geoLocations' | 'products';
@@ -44,3 +45,71 @@ export const literatureForm = () =>
4445
{ id: 'url', type: 'url', className: 'col s12', label: t('LINK') },
4546
{ id: 'description', type: 'textarea', className: 'col s12', label: t('SUMMARY') },
4647
] as UIForm<Partial<Literature>>;
48+
49+
export const crimeScriptFilterFormFactory = (
50+
products: Array<Labeled & Hierarchical>,
51+
locations: Array<Labeled & Hierarchical>,
52+
geoLocations: Array<Labeled & Hierarchical>,
53+
icon = 'filter_alt'
54+
): UIForm<CrimeScriptFilter> =>
55+
[
56+
{
57+
id: 'productIds',
58+
label: t('PRODUCTS', 2),
59+
icon,
60+
type: 'select',
61+
multiple: true,
62+
options: toOptions(products),
63+
className: 'col s6 m4',
64+
},
65+
{
66+
id: 'locationIds',
67+
label: t('LOCATIONS', 2),
68+
type: 'select',
69+
multiple: true,
70+
options: toOptions(locations),
71+
className: 'col s6 m4',
72+
},
73+
{
74+
id: 'geoLocationIds',
75+
label: t('GEOLOCATIONS', 2),
76+
type: 'select',
77+
multiple: true,
78+
options: toOptions(geoLocations),
79+
className: 'col s6 m4',
80+
},
81+
] as UIForm<CrimeScriptFilter>;
82+
83+
export const attributeFilterFormFactory = (
84+
cast: Array<Labeled & Hierarchical>,
85+
attributes: Array<Labeled & Hierarchical>,
86+
transports: Array<Labeled & Hierarchical>,
87+
icon = 'filter_alt'
88+
) =>
89+
[
90+
{
91+
id: 'roleIds',
92+
label: t('CAST', 2),
93+
icon,
94+
type: 'select',
95+
multiple: true,
96+
options: toOptions(cast),
97+
className: 'col s6 m4',
98+
},
99+
{
100+
id: 'attributeIds',
101+
label: t('ATTRIBUTES', 2),
102+
type: 'select',
103+
multiple: true,
104+
options: toOptions(attributes),
105+
className: 'col s6 m4',
106+
},
107+
{
108+
id: 'transportIds',
109+
label: t('TRANSPORTS', 2),
110+
type: 'select',
111+
multiple: true,
112+
options: toOptions(transports),
113+
className: 'col s6 m4',
114+
},
115+
] as UIForm<CrimeScriptFilter>;

packages/gui/src/services/flex-search.ts

+2-17
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,8 @@
11
import { Service } from 'meiosis-setup/types';
22
import { State } from './meiosis';
3-
import { DataModel, Hierarchical, ID, Labeled } from '../models';
3+
import { DataModel, FlexSearchResult, Hierarchical, ID, Labeled, SearchScore } from '../models';
44
import { i18n } from './translations';
5-
6-
export type FlexSearchResult = [crimeScriptIdx: number, actIdx: number, phaseIdx: number, score: number];
7-
8-
export const tokenize = (text: string = '', stopwords: string[]): string[] => {
9-
return text
10-
.replace(/[^\w\s]/g, '') // Remove punctuation
11-
.split(/\s+/) // Split into words
12-
.map((word) => word.toLowerCase()) // Convert to lowercase
13-
.filter((word) => word.length > 2 && !stopwords.includes(word)); // Exclude stopwords and empty strings
14-
};
15-
16-
export enum SearchScore {
17-
EXACT_MATCH = 3,
18-
PARENT_MATCH = 2,
19-
OTHER_MATCH = 1,
20-
}
5+
import { tokenize } from '../utils';
216

227
/**
238
* A flexible search solution:

packages/gui/src/services/lang/en.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,9 @@ export const messages = {
188188
SEARCH: 'Search...',
189189
SEARCH_TOOLTIP: 'Type / to search',
190190
HITS: {
191-
0: 'No results found',
192-
1: '1 result found',
193-
n: '{n} results found',
191+
0: 'No results found.',
192+
1: '1 result found:',
193+
n: '{n} results found:',
194194
},
195195
SYNONYMS: 'Synonyms',
196196
PARENTS: 'Parents',

packages/gui/src/services/lang/nl.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ export const messagesNL: typeof messages = {
190190
SEARCH: 'Zoek...',
191191
SEARCH_TOOLTIP: 'Type / om te zoeken',
192192
HITS: {
193-
0: 'Geen resultaten gevonden',
194-
1: '1 resultaat gevonden',
195-
n: '{n} resultaten gevonden',
193+
0: 'Geen resultaten gevonden.',
194+
1: '1 resultaat gevonden:',
195+
n: '{n} resultaten gevonden:',
196196
},
197197
SYNONYMS: 'Synoniemen',
198198
PARENTS: 'Ouders',

0 commit comments

Comments
 (0)