@devix-tecnologia/directus-extension-mapgrid 1.5.1 → 2.0.0
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/README.md +616 -14
- package/dist/index.js +1 -5
- package/package.json +75 -37
- package/src/contract/camera-tracking.contract.ts +14 -0
- package/src/contract/geolocation.contract.ts +5 -0
- package/src/contract/index.ts +4 -0
- package/src/contract/layout-options.contract.ts +54 -0
- package/src/contract/layout-query.test.ts +79 -0
- package/src/contract/layout-query.ts +65 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devix-tecnologia/directus-extension-mapgrid",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Extension for show map and grid list on Layout Directus",
|
|
5
5
|
"icon": "extension",
|
|
6
6
|
"keywords": [
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"url": "https://github.com/devix-tecnologia/directus-extension-mapgrid"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|
|
33
|
-
"node": ">=
|
|
34
|
-
"pnpm": ">=10.
|
|
33
|
+
"node": ">=22.13.1",
|
|
34
|
+
"pnpm": ">=10.15.0"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -39,9 +39,16 @@
|
|
|
39
39
|
},
|
|
40
40
|
"files": [
|
|
41
41
|
"dist/",
|
|
42
|
+
"src/contract/",
|
|
42
43
|
"package.json",
|
|
43
44
|
"pnpm-lock.yaml"
|
|
44
45
|
],
|
|
46
|
+
"exports": {
|
|
47
|
+
"./contract": {
|
|
48
|
+
"types": "./src/contract/index.ts",
|
|
49
|
+
"import": "./src/contract/index.ts"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
45
52
|
"directus:extension": {
|
|
46
53
|
"type": "layout",
|
|
47
54
|
"path": "dist/index.js",
|
|
@@ -58,51 +65,82 @@
|
|
|
58
65
|
"scripts": {
|
|
59
66
|
"build": "directus-extension build",
|
|
60
67
|
"dev": "directus-extension build -w --no-minify",
|
|
68
|
+
"validate": "directus-extension validate",
|
|
61
69
|
"link": "directus-extension link",
|
|
62
|
-
"typecheck": "tsc --noEmit",
|
|
63
|
-
"lint": "
|
|
64
|
-
"lint:fix": "
|
|
65
|
-
"format": "
|
|
66
|
-
"prepare": "husky"
|
|
70
|
+
"typecheck": "vue-tsc --noEmit && tsc --noEmit -p tsconfig.tests.json",
|
|
71
|
+
"lint": "biome check",
|
|
72
|
+
"lint:fix": "biome check --write --unsafe",
|
|
73
|
+
"format": "biome format --write src/",
|
|
74
|
+
"prepare": "husky",
|
|
75
|
+
"storybook": "storybook dev -p 6006",
|
|
76
|
+
"build-storybook": "storybook build",
|
|
77
|
+
"check:stories": "playwright test --config=playwright.stories.config.ts",
|
|
78
|
+
"test": "vitest run && vitest run --config vitest.directus-vue.config.ts",
|
|
79
|
+
"test:watch": "vitest",
|
|
80
|
+
"test:integration": "VERBOSE=true node --disable-warning=ExperimentalWarning --experimental-strip-types tests/run-docker-tests.js integration",
|
|
81
|
+
"test:e2e": "pnpm build && VERBOSE=true node --disable-warning=ExperimentalWarning --experimental-strip-types tests/run-docker-tests.js e2e",
|
|
82
|
+
"test:e2e:docker": "pnpm test:e2e",
|
|
83
|
+
"test:e2e:host": "pnpm build && VERBOSE=true node --disable-warning=ExperimentalWarning --experimental-strip-types tests/run-docker-tests.js e2e --host",
|
|
84
|
+
"screenshot": "pnpm build && docker compose -f docker-compose.test.yml --profile runner run --rm -e RUNNER_CMD=screenshot tests; status=$?; docker compose -f docker-compose.test.yml --profile runner down --remove-orphans; exit $status",
|
|
85
|
+
"test:e2e:ui": "pnpm build && VERBOSE=true node --disable-warning=ExperimentalWarning --experimental-strip-types tests/run-docker-tests.js e2e --host -- --ui",
|
|
86
|
+
"test:e2e:record": "pnpm build && VERBOSE=true node --disable-warning=ExperimentalWarning --experimental-strip-types tests/run-docker-tests.js e2e --host -- --config=playwright.record.config.ts",
|
|
87
|
+
"test:e2e:record:head": "pnpm test:e2e:record && pnpm exec playwright show-report playwright-report",
|
|
88
|
+
"test:e2e:debug": "playwright test --debug",
|
|
89
|
+
"test:e2e:headed": "playwright test --headed",
|
|
90
|
+
"test:e2e:report": "playwright show-report",
|
|
91
|
+
"test:collection": "node --disable-warning=ExperimentalWarning --experimental-strip-types scripts/test-collection.ts",
|
|
92
|
+
"demo": "node --disable-warning=ExperimentalWarning --experimental-strip-types scripts/demo.ts",
|
|
93
|
+
"test:all": "pnpm test && pnpm test:integration && pnpm test:e2e",
|
|
94
|
+
"sandcastle": "node --disable-warning=ExperimentalWarning --experimental-strip-types .sandcastle/main.ts",
|
|
95
|
+
"sandcastle:round": "node --disable-warning=ExperimentalWarning --experimental-strip-types .sandcastle/round.ts",
|
|
96
|
+
"sandcastle:queue": "node --disable-warning=ExperimentalWarning --experimental-strip-types .sandcastle/task-queue/cli.ts",
|
|
97
|
+
"sandcastle:image": "docker build -t sandcastle:$(basename \"$PWD\" | tr '[:upper:]' '[:lower:]') --build-arg AGENT_UID=$(id -u) --build-arg AGENT_GID=$(id -g) --build-arg DOCKER_GID=$(getent group docker | cut -d: -f3) -f .sandcastle/Dockerfile .sandcastle",
|
|
98
|
+
"sandcastle:clean": "node .sandcastle/clean-worktrees.mjs",
|
|
99
|
+
"typecheck:sandcastle": "tsc --noEmit -p .sandcastle/tsconfig.json",
|
|
100
|
+
"test:directus-vue": "vitest run --config vitest.directus-vue.config.ts",
|
|
101
|
+
"sandcastle:round:authenticated": "node --disable-warning=ExperimentalWarning --experimental-strip-types .sandcastle/authenticated-round.ts"
|
|
67
102
|
},
|
|
68
103
|
"devDependencies": {
|
|
69
|
-
"@
|
|
70
|
-
"@
|
|
71
|
-
"@
|
|
104
|
+
"@ai-hero/sandcastle": "^0.12.0",
|
|
105
|
+
"@biomejs/biome": "^2.5.9",
|
|
106
|
+
"@directus/constants": "^13.0.3",
|
|
107
|
+
"@directus/extensions-sdk": "^16.0.2",
|
|
108
|
+
"@directus/types": "^13.2.3",
|
|
109
|
+
"@playwright/test": "^1.62.1",
|
|
72
110
|
"@semantic-release/git": "^10.0.1",
|
|
73
|
-
"@semantic-release/github": "^
|
|
74
|
-
"@semantic-release/npm": "^
|
|
75
|
-
"@
|
|
76
|
-
"@
|
|
77
|
-
"@
|
|
78
|
-
"@
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
111
|
+
"@semantic-release/github": "^12.0.10",
|
|
112
|
+
"@semantic-release/npm": "^13.2.0",
|
|
113
|
+
"@storybook/addon-docs": "^10.5.10",
|
|
114
|
+
"@storybook/vue3-vite": "^10.5.10",
|
|
115
|
+
"@types/node": "^26.2.0",
|
|
116
|
+
"@vitejs/plugin-vue": "^6.0.4",
|
|
117
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
118
|
+
"@vue/test-utils": "^2.4.11",
|
|
119
|
+
"axios": "^1.19.0",
|
|
120
|
+
"directus-vue": "npm:vue@3.4.27",
|
|
121
|
+
"globals": "^16.4.0",
|
|
122
|
+
"happy-dom": "^20.11.6",
|
|
83
123
|
"husky": "^9.1.7",
|
|
84
|
-
"lint-staged": "^
|
|
85
|
-
"
|
|
86
|
-
"semantic-release": "^
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
124
|
+
"lint-staged": "^16.2.3",
|
|
125
|
+
"playwright": "^1.62.1",
|
|
126
|
+
"semantic-release": "^25.0.9",
|
|
127
|
+
"storybook": "^10.5.10",
|
|
128
|
+
"storytype": "^0.2.6",
|
|
129
|
+
"taskin": "^4.4.0",
|
|
130
|
+
"typescript": "^5.9.3",
|
|
131
|
+
"vitest": "^4.0.18",
|
|
132
|
+
"vue": "^3.5.22",
|
|
133
|
+
"vue-i18n": "^11.4.10",
|
|
134
|
+
"vue-router": "4.5.1",
|
|
135
|
+
"vue-tsc": "^3.3.11"
|
|
90
136
|
},
|
|
91
137
|
"dependencies": {
|
|
92
138
|
"@types/geojson": "^7946.0.16",
|
|
93
|
-
"maplibre-gl": "^5.
|
|
94
|
-
},
|
|
95
|
-
"prettier": {
|
|
96
|
-
"singleQuote": true,
|
|
97
|
-
"trailingComma": "es5",
|
|
98
|
-
"printWidth": 100,
|
|
99
|
-
"semi": true,
|
|
100
|
-
"tabWidth": 2
|
|
139
|
+
"maplibre-gl": "^5.8.0"
|
|
101
140
|
},
|
|
102
141
|
"lint-staged": {
|
|
103
142
|
"*.{ts,vue}": [
|
|
104
|
-
"
|
|
105
|
-
"prettier --write"
|
|
143
|
+
"biome check --write"
|
|
106
144
|
]
|
|
107
145
|
},
|
|
108
146
|
"release": {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much the camera chases the current record.
|
|
3
|
+
*
|
|
4
|
+
* It lives in the contract because it is stored in the Directus preset: a map
|
|
5
|
+
* implementing `IMapCenterer` has to read the same three states.
|
|
6
|
+
*
|
|
7
|
+
* Navigation apps call this *follow mode*; the name "autofocus" was dropped on
|
|
8
|
+
* purpose, because in photography it is lens sharpness, not framing.
|
|
9
|
+
*
|
|
10
|
+
* - `off` — the map stays where the person left it;
|
|
11
|
+
* - `follow` — moves only when the record leaves the visible area;
|
|
12
|
+
* - `center` — keeps the record in the middle at every step.
|
|
13
|
+
*/
|
|
14
|
+
export type CameraTracking = 'off' | 'follow' | 'center';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CameraTracking } from './camera-tracking.contract';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where the current record's arrow takes its direction from.
|
|
5
|
+
*
|
|
6
|
+
* - `off` — no arrow;
|
|
7
|
+
* - `field` — a numeric field of the record, in degrees clockwise from north;
|
|
8
|
+
* - `computed` — the bearing from the record visited before to the current one.
|
|
9
|
+
*/
|
|
10
|
+
export type HeadingSource = 'off' | 'field' | 'computed';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* How long playback waits between records.
|
|
14
|
+
*
|
|
15
|
+
* - `fixed` — the same `playbackInterval` for every step;
|
|
16
|
+
* - `realtime` — the time between the records' own instants, sped up by `playbackSpeed`.
|
|
17
|
+
*/
|
|
18
|
+
export type PlaybackTiming = 'fixed' | 'realtime';
|
|
19
|
+
|
|
20
|
+
/** What the MapGrid stores in the Directus preset. */
|
|
21
|
+
export interface LayoutOptions {
|
|
22
|
+
/** The embedded tabular layout's configuration; the content is Directus'. */
|
|
23
|
+
tabular?: Record<string, unknown>;
|
|
24
|
+
/** The embedded map layout's configuration; the content is Directus'. */
|
|
25
|
+
map?: Record<string, unknown>;
|
|
26
|
+
/** Zoom the map in when a record is focused, instead of keeping the zoom. */
|
|
27
|
+
zoomOnClick?: boolean;
|
|
28
|
+
/** How much the camera chases the current record. */
|
|
29
|
+
cameraTracking?: CameraTracking;
|
|
30
|
+
/** Seconds between records during playback. */
|
|
31
|
+
playbackInterval?: number;
|
|
32
|
+
/** Fixed beat, or the data's own clock. */
|
|
33
|
+
playbackTiming?: PlaybackTiming;
|
|
34
|
+
/** How many times faster than the data's clock, in `realtime`. */
|
|
35
|
+
playbackSpeed?: number;
|
|
36
|
+
/** The date/time field the data's clock is read from; empty, the sort's is suggested. */
|
|
37
|
+
playbackTimeField?: string;
|
|
38
|
+
/** Where the current record's arrow takes its direction from. */
|
|
39
|
+
headingSource?: HeadingSource;
|
|
40
|
+
/** The numeric field read when `headingSource` is `field`. */
|
|
41
|
+
headingField?: string;
|
|
42
|
+
/** How many visited records the trail joins; `0` turns it off. */
|
|
43
|
+
trailLength?: number;
|
|
44
|
+
/** Shows the focused record in a card over the map. */
|
|
45
|
+
showCard?: boolean;
|
|
46
|
+
/** The file field whose image the card shows and enlarges. */
|
|
47
|
+
cardImageField?: string;
|
|
48
|
+
/** The fields the card lists; empty, it lists the grid's columns. */
|
|
49
|
+
cardFields?: string[];
|
|
50
|
+
/** Opens the tour of the features on the first visit of each browser. */
|
|
51
|
+
tourOnFirstVisit?: boolean;
|
|
52
|
+
/** The map's share of the height, against the grid's; the rest is the grid's. */
|
|
53
|
+
mapShare?: number;
|
|
54
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import type { LayoutQuery } from '../types';
|
|
4
|
+
import { useWritableLayoutQuery } from './layout-query';
|
|
5
|
+
|
|
6
|
+
const queryRef = (initial: Partial<LayoutQuery> | null = null) =>
|
|
7
|
+
ref(initial as LayoutQuery | null);
|
|
8
|
+
|
|
9
|
+
describe('useWritableLayoutQuery — reading', () => {
|
|
10
|
+
it('falls back to the first page when the preset has none', () => {
|
|
11
|
+
expect(useWritableLayoutQuery(queryRef()).page.value).toBe(1);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('falls back to a page size of 25, which is what Directus itself defaults to', () => {
|
|
15
|
+
expect(useWritableLayoutQuery(queryRef()).limit.value).toBe(25);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('falls back to no sort, and not to an arbitrary field', () => {
|
|
19
|
+
expect(useWritableLayoutQuery(queryRef()).sort.value).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('reads what the preset stored', () => {
|
|
23
|
+
const query = useWritableLayoutQuery(queryRef({ page: 3, limit: 50, sort: ['-name'] }));
|
|
24
|
+
|
|
25
|
+
expect(query.page.value).toBe(3);
|
|
26
|
+
expect(query.limit.value).toBe(50);
|
|
27
|
+
expect(query.sort.value).toEqual(['-name']);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('useWritableLayoutQuery — fields is where Directus keeps the displayed columns', () => {
|
|
32
|
+
it('reads the stored fields', () => {
|
|
33
|
+
expect(useWritableLayoutQuery(queryRef({ fields: ['name', 'city'] })).fields.value).toEqual([
|
|
34
|
+
'name',
|
|
35
|
+
'city',
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns undefined when nothing was stored, so the caller can fall back', () => {
|
|
40
|
+
expect(useWritableLayoutQuery(queryRef()).fields.value).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('writes the fields back, which is what choosing a column has to persist', () => {
|
|
44
|
+
const source = queryRef({ page: 1 });
|
|
45
|
+
useWritableLayoutQuery(source).fields.value = ['name', 'status'];
|
|
46
|
+
|
|
47
|
+
expect(source.value?.fields).toEqual(['name', 'status']);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('useWritableLayoutQuery — writing', () => {
|
|
52
|
+
it('writes the page back to the preset, so pagination survives a reload', () => {
|
|
53
|
+
const source = queryRef({ page: 1, limit: 25 });
|
|
54
|
+
useWritableLayoutQuery(source).page.value = 4;
|
|
55
|
+
|
|
56
|
+
expect(source.value?.page).toBe(4);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('writes the sort back, which is what clicking a column header needs', () => {
|
|
60
|
+
const source = queryRef({ page: 1 });
|
|
61
|
+
useWritableLayoutQuery(source).sort.value = ['-status'];
|
|
62
|
+
|
|
63
|
+
expect(source.value?.sort).toEqual(['-status']);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('preserves the other fields when writing one, instead of replacing the preset', () => {
|
|
67
|
+
const source = queryRef({ page: 2, limit: 50, sort: ['name'], search: 'abc' });
|
|
68
|
+
useWritableLayoutQuery(source).page.value = 3;
|
|
69
|
+
|
|
70
|
+
expect(source.value).toMatchObject({ page: 3, limit: 50, sort: ['name'], search: 'abc' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('writes onto an absent preset instead of throwing, which is the state of a fresh layout', () => {
|
|
74
|
+
const source = queryRef(null);
|
|
75
|
+
useWritableLayoutQuery(source).sort.value = ['name'];
|
|
76
|
+
|
|
77
|
+
expect(source.value?.sort).toEqual(['name']);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { computed, type Ref, type WritableComputedRef } from 'vue';
|
|
2
|
+
import type { LayoutQuery } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The parts of the layout query the extension both reads and writes.
|
|
6
|
+
*
|
|
7
|
+
* Pagination and sorting are not display preferences: they belong to the query
|
|
8
|
+
* the layout hands Directus, and they have to survive a reload. Keeping them as
|
|
9
|
+
* read-only computeds meant the grid could show a sortable header that wrote
|
|
10
|
+
* nowhere, and the map had no way to advance the page on its own.
|
|
11
|
+
*
|
|
12
|
+
* Extracted from the layout's `setup` so it can be tested with a plain ref,
|
|
13
|
+
* without the Directus composables.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** What Directus itself defaults a layout's page size to. */
|
|
17
|
+
const DEFAULT_LIMIT = 25;
|
|
18
|
+
|
|
19
|
+
export interface WritableLayoutQuery {
|
|
20
|
+
page: WritableComputedRef<number>;
|
|
21
|
+
limit: WritableComputedRef<number>;
|
|
22
|
+
sort: WritableComputedRef<string[]>;
|
|
23
|
+
/**
|
|
24
|
+
* The columns the grid shows.
|
|
25
|
+
*
|
|
26
|
+
* This lives in the query, and not in the layout options, because that is
|
|
27
|
+
* where Directus itself keeps it — its tabular layout stores the displayed
|
|
28
|
+
* columns in `layoutQuery.fields` and derives what to actually fetch from
|
|
29
|
+
* them. Undefined means nothing was chosen, so the caller can fall back to a
|
|
30
|
+
* legacy preset or to what it detected from the collection.
|
|
31
|
+
*/
|
|
32
|
+
fields: WritableComputedRef<string[] | undefined>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const useWritableLayoutQuery = (
|
|
36
|
+
layoutQuery: Ref<LayoutQuery | null | undefined>
|
|
37
|
+
): WritableLayoutQuery => {
|
|
38
|
+
/**
|
|
39
|
+
* Writes one field without disturbing the rest. The preset also carries
|
|
40
|
+
* `filter`, `search` and `fields`, and replacing the object would drop
|
|
41
|
+
* whatever this layout did not set.
|
|
42
|
+
*/
|
|
43
|
+
const write = <Key extends keyof LayoutQuery>(key: Key, value: LayoutQuery[Key]): void => {
|
|
44
|
+
layoutQuery.value = { ...(layoutQuery.value ?? ({} as LayoutQuery)), [key]: value };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
page: computed({
|
|
49
|
+
get: () => layoutQuery.value?.page || 1,
|
|
50
|
+
set: (value) => write('page', value),
|
|
51
|
+
}),
|
|
52
|
+
limit: computed({
|
|
53
|
+
get: () => layoutQuery.value?.limit || DEFAULT_LIMIT,
|
|
54
|
+
set: (value) => write('limit', value),
|
|
55
|
+
}),
|
|
56
|
+
sort: computed({
|
|
57
|
+
get: () => layoutQuery.value?.sort ?? [],
|
|
58
|
+
set: (value) => write('sort', value),
|
|
59
|
+
}),
|
|
60
|
+
fields: computed({
|
|
61
|
+
get: () => layoutQuery.value?.fields,
|
|
62
|
+
set: (value) => write('fields', value ?? []),
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
};
|