changeledger 0.3.0 → 0.5.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/AGENTS.md +13 -7
- package/README.md +22 -13
- package/bin/changeledger.mjs +10 -2
- package/package.json +1 -1
- package/src/check.mjs +35 -9
- package/src/commands/agent.mjs +1 -1
- package/src/commands/check.mjs +1 -1
- package/src/commands/context.mjs +65 -0
- package/src/commands/init.mjs +3 -6
- package/src/commands/new.mjs +1 -1
- package/src/commands/register.mjs +10 -6
- package/src/commands/view.mjs +10 -1
- package/src/config.mjs +6 -3
- package/src/contract.mjs +86 -50
- package/src/paths.mjs +1 -1
- package/src/registry.mjs +24 -1
- package/src/repo.mjs +7 -0
- package/src/viewer/domain.mjs +136 -3
- package/src/viewer/public/api.js +26 -0
- package/src/viewer/public/app-state.js +144 -5
- package/src/viewer/public/app.js +299 -11
- package/src/viewer/public/index.html +2 -0
- package/src/viewer/public/styles.css +202 -0
- package/src/viewer/server/router.mjs +50 -11
- package/templates/config.yml +4 -3
- package/templates/contract/blocked.md +6 -0
- package/templates/contract/close.md +18 -0
- package/templates/contract/core.md +64 -0
- package/templates/contract/discarded.md +5 -0
- package/templates/contract/implement.md +52 -0
- package/templates/contract/readiness.md +19 -0
- package/templates/contract/release.md +15 -0
- package/templates/contract/review.md +19 -0
- package/templates/contract/spec.md +94 -0
- package/templates/contract/validation.md +9 -0
- package/templates/AGENTS.md +0 -540
package/src/registry.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import fs from 'node:fs';
|
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { withFileLock, writeFileAtomic } from './atomic-write.mjs';
|
|
10
|
+
import { loadConfig } from './config.mjs';
|
|
10
11
|
|
|
11
12
|
export function registryDir() {
|
|
12
13
|
return path.join(process.env.CHANGELEDGER_HOME || os.homedir(), '.changeledger');
|
|
@@ -42,7 +43,18 @@ export function register({ id, name, path: repoPath }) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export function listProjects() {
|
|
45
|
-
return Object.entries(readRegistry()).map(([id,
|
|
46
|
+
return Object.entries(readRegistry()).map(([id, value]) => {
|
|
47
|
+
let name = value.name;
|
|
48
|
+
try {
|
|
49
|
+
const config = loadConfig(path.join(value.path, '.changeledger'));
|
|
50
|
+
if (String(config.project_id) === id && typeof config.project_name === 'string') {
|
|
51
|
+
name = config.project_name;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Missing projects keep their last registered display name.
|
|
55
|
+
}
|
|
56
|
+
return { id, name, path: value.path };
|
|
57
|
+
});
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
export function remove(id) {
|
|
@@ -53,3 +65,14 @@ export function remove(id) {
|
|
|
53
65
|
writeRegistry(reg);
|
|
54
66
|
});
|
|
55
67
|
}
|
|
68
|
+
|
|
69
|
+
export function update(id, values) {
|
|
70
|
+
fs.mkdirSync(registryDir(), { recursive: true });
|
|
71
|
+
return withFileLock(registryPath(), () => {
|
|
72
|
+
const reg = readRegistry();
|
|
73
|
+
if (!reg[id]) throw new Error(`no registered project "${id}"`);
|
|
74
|
+
reg[id] = { ...reg[id], ...values };
|
|
75
|
+
writeRegistry(reg);
|
|
76
|
+
return reg[id];
|
|
77
|
+
});
|
|
78
|
+
}
|
package/src/repo.mjs
CHANGED
|
@@ -47,6 +47,13 @@ export function loadRepo(start = process.cwd()) {
|
|
|
47
47
|
}
|
|
48
48
|
const repoRoot = path.dirname(changeledgerDir);
|
|
49
49
|
const config = loadConfig(changeledgerDir);
|
|
50
|
+
return loadRepoWithConfig(repoRoot, changeledgerDir, config);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Loads repository content using an already parsed candidate config. The viewer
|
|
54
|
+
// uses this before replacing config.yml so changes to configured directories are
|
|
55
|
+
// validated against the content they would actually expose after the save.
|
|
56
|
+
export function loadRepoWithConfig(repoRoot, changeledgerDir, config) {
|
|
50
57
|
const changesDir = resolveRepoPath(repoRoot, config.changes_dir, 'changes_dir');
|
|
51
58
|
|
|
52
59
|
const changes = [];
|
package/src/viewer/domain.mjs
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import { mutateFileAtomic } from '../atomic-write.mjs';
|
|
5
|
+
import { checkRepo } from '../check.mjs';
|
|
3
6
|
import { status as applyStatusCmd, validation as applyValidation } from '../commands/agent.mjs';
|
|
4
|
-
import { findChangeledgerDir, loadConfig } from '../config.mjs';
|
|
7
|
+
import { findChangeledgerDir, loadConfig, resolveRepoPath, resolveSpecsDir } from '../config.mjs';
|
|
5
8
|
import { computeMetrics } from '../metrics.mjs';
|
|
6
9
|
import { nowUtc } from '../paths.mjs';
|
|
7
|
-
import { listProjects } from '../registry.mjs';
|
|
8
|
-
import { loadRepo } from '../repo.mjs';
|
|
10
|
+
import { listProjects, remove, update } from '../registry.mjs';
|
|
11
|
+
import { loadRepo, loadRepoWithConfig } from '../repo.mjs';
|
|
12
|
+
import { parseYaml } from '../yaml.mjs';
|
|
9
13
|
|
|
10
14
|
// Serializes a loaded repo into the flat shape the UI consumes.
|
|
11
15
|
export function serialize(repo) {
|
|
@@ -131,3 +135,132 @@ export function changeStatus(projects, { project, id, status, reason }) {
|
|
|
131
135
|
return { code: 400, body: { error: e.message } };
|
|
132
136
|
}
|
|
133
137
|
}
|
|
138
|
+
|
|
139
|
+
const revision = (text) => crypto.createHash('sha256').update(text).digest('hex');
|
|
140
|
+
|
|
141
|
+
function projectFor(projects, id) {
|
|
142
|
+
const project = projects.find((item) => item.id === id);
|
|
143
|
+
if (!project) return { code: 404, body: { error: `no project "${id}"` } };
|
|
144
|
+
if (!project.alive) return { code: 410, body: { error: 'project path is gone' } };
|
|
145
|
+
return { project };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function readProjectConfig(projects, id) {
|
|
149
|
+
const found = projectFor(projects, id);
|
|
150
|
+
if (!found.project) return found;
|
|
151
|
+
const file = path.join(found.project.path, '.changeledger', 'config.yml');
|
|
152
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
153
|
+
return { code: 200, body: { content, revision: revision(content) } };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function saveProjectConfig(projects, payload, { mutateConfig = mutateFileAtomic } = {}) {
|
|
157
|
+
const found = projectFor(projects, payload.project);
|
|
158
|
+
if (!found.project) return found;
|
|
159
|
+
if (typeof payload.content !== 'string' || typeof payload.revision !== 'string') {
|
|
160
|
+
return { code: 400, body: { error: 'content and revision are required' } };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let candidate;
|
|
164
|
+
try {
|
|
165
|
+
candidate = parseYaml(payload.content);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return { code: 400, body: { error: error.message } };
|
|
168
|
+
}
|
|
169
|
+
if (String(candidate.project_id ?? '') !== String(found.project.id)) {
|
|
170
|
+
return { code: 400, body: { error: 'project_id cannot be changed from the viewer' } };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let repo;
|
|
174
|
+
try {
|
|
175
|
+
repo = loadRepo(found.project.path);
|
|
176
|
+
} catch {
|
|
177
|
+
return { code: 400, body: { error: 'unable to load the current project configuration' } };
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
resolveRepoPath(repo.repoRoot, candidate.changes_dir, 'changes_dir');
|
|
181
|
+
resolveSpecsDir(repo.repoRoot, candidate);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return { code: 400, body: { error: error.message } };
|
|
184
|
+
}
|
|
185
|
+
let candidateRepo;
|
|
186
|
+
try {
|
|
187
|
+
candidateRepo = loadRepoWithConfig(repo.repoRoot, repo.changeledgerDir, candidate);
|
|
188
|
+
} catch {
|
|
189
|
+
return { code: 400, body: { error: 'candidate configuration cannot load the repository' } };
|
|
190
|
+
}
|
|
191
|
+
let errors;
|
|
192
|
+
try {
|
|
193
|
+
({ errors } = checkRepo(candidateRepo));
|
|
194
|
+
} catch {
|
|
195
|
+
return {
|
|
196
|
+
code: 400,
|
|
197
|
+
body: { error: 'candidate configuration violates the ChangeLedger contract' },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (errors.length) return { code: 400, body: { error: errors[0].message } };
|
|
201
|
+
|
|
202
|
+
const file = path.join(repo.changeledgerDir, 'config.yml');
|
|
203
|
+
const projectName =
|
|
204
|
+
typeof candidate.project_name === 'string' && candidate.project_name.trim()
|
|
205
|
+
? candidate.project_name
|
|
206
|
+
: found.project.name;
|
|
207
|
+
try {
|
|
208
|
+
mutateConfig(file, (before) => {
|
|
209
|
+
if (revision(before) !== payload.revision) {
|
|
210
|
+
throw new Error('configuration changed on disk; reload before saving');
|
|
211
|
+
}
|
|
212
|
+
return payload.content;
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error.message === 'configuration changed on disk; reload before saving') {
|
|
216
|
+
return { code: 409, body: { error: error.message } };
|
|
217
|
+
}
|
|
218
|
+
return { code: 400, body: { error: 'unable to save project configuration' } };
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
code: 200,
|
|
222
|
+
body: { ok: true, name: projectName, revision: revision(payload.content) },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function repairProjectPath(projects, payload, { localOnly = false } = {}) {
|
|
227
|
+
if (localOnly)
|
|
228
|
+
return { code: 403, body: { error: 'registry management is unavailable in local mode' } };
|
|
229
|
+
const project = projects.find((item) => item.id === payload.project);
|
|
230
|
+
if (!project) return { code: 404, body: { error: `no project "${payload.project}"` } };
|
|
231
|
+
if (typeof payload.path !== 'string' || !path.isAbsolute(payload.path)) {
|
|
232
|
+
return { code: 400, body: { error: 'project path must be absolute' } };
|
|
233
|
+
}
|
|
234
|
+
const root = path.resolve(payload.path);
|
|
235
|
+
let config;
|
|
236
|
+
try {
|
|
237
|
+
config = loadConfig(path.join(root, '.changeledger'));
|
|
238
|
+
} catch {
|
|
239
|
+
return { code: 400, body: { error: 'project path is not a ChangeLedger repository' } };
|
|
240
|
+
}
|
|
241
|
+
if (String(config.project_id ?? '') !== String(project.id)) {
|
|
242
|
+
return { code: 400, body: { error: 'project path belongs to a different project_id' } };
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
update(project.id, { name: config.project_name ?? project.name, path: root });
|
|
246
|
+
} catch {
|
|
247
|
+
return { code: 400, body: { error: 'unable to update project registry' } };
|
|
248
|
+
}
|
|
249
|
+
return { code: 200, body: { ok: true } };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function unregisterProject(projects, payload, { localOnly = false } = {}) {
|
|
253
|
+
if (localOnly)
|
|
254
|
+
return { code: 403, body: { error: 'registry management is unavailable in local mode' } };
|
|
255
|
+
const project = projects.find((item) => item.id === payload.project);
|
|
256
|
+
if (!project) return { code: 404, body: { error: `no project "${payload.project}"` } };
|
|
257
|
+
if (payload.confirm !== project.name) {
|
|
258
|
+
return { code: 400, body: { error: `type "${project.name}" to confirm` } };
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
remove(project.id);
|
|
262
|
+
} catch {
|
|
263
|
+
return { code: 400, body: { error: 'unable to update project registry' } };
|
|
264
|
+
}
|
|
265
|
+
return { code: 200, body: { ok: true } };
|
|
266
|
+
}
|
package/src/viewer/public/api.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export const getProjects = () => fetch('/api/projects').then((r) => r.json());
|
|
2
2
|
|
|
3
|
+
export const getProjectConfig = (project) =>
|
|
4
|
+
fetch(`/api/project-config?project=${encodeURIComponent(project)}`).then(async (response) => {
|
|
5
|
+
const body = await response.json();
|
|
6
|
+
if (!response.ok) throw new Error(body.error || `HTTP ${response.status}`);
|
|
7
|
+
return body;
|
|
8
|
+
});
|
|
9
|
+
|
|
3
10
|
export const getRepo = async (project) => {
|
|
4
11
|
const res = await fetch(`/api/repo?project=${encodeURIComponent(project)}`);
|
|
5
12
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
@@ -23,3 +30,22 @@ export const postStatus = (project, id, status, reason) =>
|
|
|
23
30
|
},
|
|
24
31
|
body: JSON.stringify({ project, id, status, ...(reason ? { reason } : {}) }),
|
|
25
32
|
});
|
|
33
|
+
|
|
34
|
+
const postProject = (route, body) =>
|
|
35
|
+
fetch(route, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
'x-changeledger-token': window.__CHANGELEDGER_TOKEN__,
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const postProjectConfig = (project, content, revision) =>
|
|
45
|
+
postProject('/api/project-config', { project, content, revision });
|
|
46
|
+
|
|
47
|
+
export const postProjectPath = (project, path) =>
|
|
48
|
+
postProject('/api/project-path', { project, path });
|
|
49
|
+
|
|
50
|
+
export const postProjectRemove = (project, confirm) =>
|
|
51
|
+
postProject('/api/project-remove', { project, confirm });
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
export const VIEWER_STATE_KEY = 'changeledger.viewer-state.v1';
|
|
2
|
+
|
|
3
|
+
const VALID_VIEWS = new Set(['board', 'table', 'graph', 'specs', 'metrics', 'projects']);
|
|
4
|
+
const VALID_SORT_KEYS = new Set(['id', 'title', 'type', 'status', 'progress', 'deps']);
|
|
5
|
+
let storage = null;
|
|
6
|
+
|
|
7
|
+
const emptyProjectFilters = () => ({
|
|
8
|
+
type: 'all',
|
|
9
|
+
owner: 'all',
|
|
10
|
+
statuses: [],
|
|
11
|
+
showArchived: false,
|
|
12
|
+
showDiscarded: false,
|
|
13
|
+
});
|
|
14
|
+
|
|
1
15
|
export const state = {
|
|
2
16
|
repo: null,
|
|
3
17
|
lastJson: '',
|
|
@@ -9,14 +23,129 @@ export const state = {
|
|
|
9
23
|
showArchived: false,
|
|
10
24
|
showDiscarded: false,
|
|
11
25
|
},
|
|
26
|
+
projectFilters: {},
|
|
12
27
|
currentView: 'board',
|
|
13
28
|
sortKey: 'id',
|
|
14
29
|
sortDir: 1,
|
|
15
30
|
currentProject: null,
|
|
16
31
|
projectsList: [],
|
|
32
|
+
localOnly: false,
|
|
17
33
|
globalMode: false,
|
|
18
34
|
};
|
|
19
35
|
|
|
36
|
+
function currentProjectFilters() {
|
|
37
|
+
return {
|
|
38
|
+
type: state.filters.type,
|
|
39
|
+
owner: state.filters.owner,
|
|
40
|
+
statuses: [...state.filters.statuses],
|
|
41
|
+
showArchived: state.filters.showArchived,
|
|
42
|
+
showDiscarded: state.filters.showDiscarded,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function saveCurrentProjectFilters() {
|
|
47
|
+
if (state.currentProject) state.projectFilters[state.currentProject] = currentProjectFilters();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function applyProjectFilters(id) {
|
|
51
|
+
const filters = state.projectFilters[id] ?? emptyProjectFilters();
|
|
52
|
+
state.filters.type = typeof filters.type === 'string' ? filters.type : 'all';
|
|
53
|
+
state.filters.owner = typeof filters.owner === 'string' ? filters.owner : 'all';
|
|
54
|
+
state.filters.statuses = new Set(
|
|
55
|
+
Array.isArray(filters.statuses)
|
|
56
|
+
? filters.statuses.filter((value) => typeof value === 'string')
|
|
57
|
+
: [],
|
|
58
|
+
);
|
|
59
|
+
state.filters.showArchived = filters.showArchived === true;
|
|
60
|
+
state.filters.showDiscarded = filters.showDiscarded === true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function serializeViewerState() {
|
|
64
|
+
saveCurrentProjectFilters();
|
|
65
|
+
return {
|
|
66
|
+
version: 1,
|
|
67
|
+
currentProject: state.currentProject,
|
|
68
|
+
currentView: state.currentView,
|
|
69
|
+
globalMode: state.globalMode,
|
|
70
|
+
text: state.filters.text,
|
|
71
|
+
sortKey: state.sortKey,
|
|
72
|
+
sortDir: state.sortDir,
|
|
73
|
+
projects: state.projectFilters,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function persistViewerState() {
|
|
78
|
+
if (!storage) return false;
|
|
79
|
+
try {
|
|
80
|
+
storage.setItem(VIEWER_STATE_KEY, JSON.stringify(serializeViewerState()));
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function restoreViewerState(storageLike) {
|
|
88
|
+
storage = storageLike;
|
|
89
|
+
let snapshot;
|
|
90
|
+
try {
|
|
91
|
+
snapshot = JSON.parse(storage.getItem(VIEWER_STATE_KEY) || 'null');
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (snapshot?.version !== 1 || typeof snapshot !== 'object') return false;
|
|
96
|
+
if (typeof snapshot.currentProject === 'string') state.currentProject = snapshot.currentProject;
|
|
97
|
+
if (typeof snapshot.currentView === 'string') state.currentView = snapshot.currentView;
|
|
98
|
+
state.globalMode = snapshot.globalMode === true;
|
|
99
|
+
if (typeof snapshot.text === 'string') state.filters.text = snapshot.text;
|
|
100
|
+
if (typeof snapshot.sortKey === 'string') state.sortKey = snapshot.sortKey;
|
|
101
|
+
if (snapshot.sortDir === 1 || snapshot.sortDir === -1) state.sortDir = snapshot.sortDir;
|
|
102
|
+
if (
|
|
103
|
+
snapshot.projects &&
|
|
104
|
+
typeof snapshot.projects === 'object' &&
|
|
105
|
+
!Array.isArray(snapshot.projects)
|
|
106
|
+
) {
|
|
107
|
+
state.projectFilters = snapshot.projects;
|
|
108
|
+
}
|
|
109
|
+
if (state.currentProject) applyProjectFilters(state.currentProject);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function initializeProjects(projects, serverCurrent) {
|
|
114
|
+
state.projectsList = projects;
|
|
115
|
+
const alive = new Set(projects.filter((project) => project.alive).map((project) => project.id));
|
|
116
|
+
const selected = alive.has(state.currentProject)
|
|
117
|
+
? state.currentProject
|
|
118
|
+
: alive.has(serverCurrent)
|
|
119
|
+
? serverCurrent
|
|
120
|
+
: (projects.find((project) => project.alive)?.id ?? null);
|
|
121
|
+
if (selected !== state.currentProject) {
|
|
122
|
+
saveCurrentProjectFilters();
|
|
123
|
+
state.currentProject = selected;
|
|
124
|
+
if (selected) applyProjectFilters(selected);
|
|
125
|
+
}
|
|
126
|
+
if (!selected) state.globalMode = false;
|
|
127
|
+
persistViewerState();
|
|
128
|
+
return selected;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function normalizeRepoState(repo) {
|
|
132
|
+
if (!VALID_VIEWS.has(state.currentView)) state.currentView = 'board';
|
|
133
|
+
if (!VALID_SORT_KEYS.has(state.sortKey)) {
|
|
134
|
+
state.sortKey = 'id';
|
|
135
|
+
state.sortDir = 1;
|
|
136
|
+
}
|
|
137
|
+
if (state.sortDir !== 1 && state.sortDir !== -1) state.sortDir = 1;
|
|
138
|
+
if (!repo.types.includes(state.filters.type)) state.filters.type = 'all';
|
|
139
|
+
const owners = new Set(repo.changes.map((change) => change.owner).filter(Boolean));
|
|
140
|
+
if (state.filters.owner !== 'all' && !owners.has(state.filters.owner))
|
|
141
|
+
state.filters.owner = 'all';
|
|
142
|
+
const statuses = new Set(repo.statuses);
|
|
143
|
+
state.filters.statuses = new Set(
|
|
144
|
+
[...state.filters.statuses].filter((status) => statuses.has(status)),
|
|
145
|
+
);
|
|
146
|
+
persistViewerState();
|
|
147
|
+
}
|
|
148
|
+
|
|
20
149
|
export function setRepo(json) {
|
|
21
150
|
state.lastJson = json;
|
|
22
151
|
state.repo = JSON.parse(json);
|
|
@@ -28,19 +157,23 @@ export function invalidateCache() {
|
|
|
28
157
|
|
|
29
158
|
export function setTextFilter(text) {
|
|
30
159
|
state.filters.text = text;
|
|
160
|
+
persistViewerState();
|
|
31
161
|
}
|
|
32
162
|
|
|
33
163
|
export function setTypeFilter(type) {
|
|
34
164
|
state.filters.type = type;
|
|
165
|
+
persistViewerState();
|
|
35
166
|
}
|
|
36
167
|
|
|
37
168
|
export function setOwnerFilter(owner) {
|
|
38
169
|
state.filters.owner = owner;
|
|
170
|
+
persistViewerState();
|
|
39
171
|
}
|
|
40
172
|
|
|
41
173
|
export function toggleStatusFilter(status) {
|
|
42
174
|
if (state.filters.statuses.has(status)) state.filters.statuses.delete(status);
|
|
43
175
|
else state.filters.statuses.add(status);
|
|
176
|
+
persistViewerState();
|
|
44
177
|
return state.filters.statuses.has(status);
|
|
45
178
|
}
|
|
46
179
|
|
|
@@ -48,29 +181,33 @@ export function clearStatusFilters() {
|
|
|
48
181
|
state.filters.statuses.clear();
|
|
49
182
|
state.filters.showArchived = false;
|
|
50
183
|
state.filters.showDiscarded = false;
|
|
184
|
+
persistViewerState();
|
|
51
185
|
}
|
|
52
186
|
|
|
53
187
|
export function toggleShowArchived() {
|
|
54
188
|
state.filters.showArchived = !state.filters.showArchived;
|
|
189
|
+
persistViewerState();
|
|
55
190
|
return state.filters.showArchived;
|
|
56
191
|
}
|
|
57
192
|
|
|
58
193
|
export function toggleShowDiscarded() {
|
|
59
194
|
state.filters.showDiscarded = !state.filters.showDiscarded;
|
|
195
|
+
persistViewerState();
|
|
60
196
|
return state.filters.showDiscarded;
|
|
61
197
|
}
|
|
62
198
|
|
|
63
|
-
export function setView(
|
|
64
|
-
state.currentView =
|
|
199
|
+
export function setView(view) {
|
|
200
|
+
state.currentView = view;
|
|
65
201
|
state.globalMode = false;
|
|
202
|
+
persistViewerState();
|
|
66
203
|
}
|
|
67
204
|
|
|
68
205
|
export function selectProject(id) {
|
|
206
|
+
saveCurrentProjectFilters();
|
|
69
207
|
state.currentProject = id;
|
|
70
208
|
state.lastJson = '';
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
state.filters.statuses.clear();
|
|
209
|
+
applyProjectFilters(id);
|
|
210
|
+
persistViewerState();
|
|
74
211
|
}
|
|
75
212
|
|
|
76
213
|
export function setSortKey(key) {
|
|
@@ -79,9 +216,11 @@ export function setSortKey(key) {
|
|
|
79
216
|
state.sortKey = key;
|
|
80
217
|
state.sortDir = 1;
|
|
81
218
|
}
|
|
219
|
+
persistViewerState();
|
|
82
220
|
}
|
|
83
221
|
|
|
84
222
|
export function toggleGlobalMode() {
|
|
85
223
|
state.globalMode = !state.globalMode;
|
|
224
|
+
persistViewerState();
|
|
86
225
|
return state.globalMode;
|
|
87
226
|
}
|