sh3-core 0.13.1 → 0.13.3
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/dist/BrandSlot.svelte +62 -13
- package/dist/__test__/setup-dom.js +5 -0
- package/dist/actions/MenuButton.svelte +2 -1
- package/dist/actions/contextMenuModel.d.ts +1 -1
- package/dist/actions/contextMenuModel.js +2 -1
- package/dist/actions/dispatcher.svelte.d.ts +1 -1
- package/dist/actions/dispatcher.svelte.js +2 -1
- package/dist/actions/listActive.d.ts +1 -1
- package/dist/actions/listActive.js +2 -1
- package/dist/actions/listeners.d.ts +1 -1
- package/dist/actions/listeners.js +6 -5
- package/dist/actions/menuBarModel.js +3 -2
- package/dist/actions/paletteModel.js +2 -1
- package/dist/actions/resolveLabel.test.js +14 -0
- package/dist/actions/types.d.ts +12 -1
- package/dist/actions/types.js +7 -1
- package/dist/api.d.ts +3 -0
- package/dist/api.js +3 -0
- package/dist/app/store/AppUpdateAvailableModal.svelte +87 -0
- package/dist/app/store/AppUpdateAvailableModal.svelte.d.ts +11 -0
- package/dist/app/store/InstalledView.svelte +8 -54
- package/dist/app/store/UninstallAppDialog.svelte +86 -0
- package/dist/app/store/UninstallAppDialog.svelte.d.ts +10 -0
- package/dist/app/store/permissionConfirm.d.ts +4 -0
- package/dist/app/store/permissionConfirm.js +28 -0
- package/dist/app/store/storeShard.svelte.d.ts +8 -1
- package/dist/app/store/storeShard.svelte.js +42 -9
- package/dist/app/store/updatePackage.test.d.ts +1 -0
- package/dist/app/store/updatePackage.test.js +34 -0
- package/dist/app/store/verbs.d.ts +1 -0
- package/dist/app/store/verbs.js +79 -5
- package/dist/app/store/verbs.test.d.ts +1 -0
- package/dist/app/store/verbs.test.js +56 -0
- package/dist/app-appearance/AppAppearanceModal.svelte +174 -0
- package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +8 -0
- package/dist/app-appearance/appearanceShard.svelte.d.ts +2 -0
- package/dist/app-appearance/appearanceShard.svelte.js +61 -0
- package/dist/app-appearance/appearanceState.svelte.d.ts +15 -0
- package/dist/app-appearance/appearanceState.svelte.js +59 -0
- package/dist/app-appearance/appearanceState.test.d.ts +1 -0
- package/dist/app-appearance/appearanceState.test.js +30 -0
- package/dist/app-appearance/index.d.ts +3 -0
- package/dist/app-appearance/index.js +2 -0
- package/dist/app-appearance/types.d.ts +11 -0
- package/dist/app-appearance/types.js +1 -0
- package/dist/apps/lifecycle.js +10 -2
- package/dist/apps/types.d.ts +18 -4
- package/dist/apps/workspace-rekey.d.ts +1 -0
- package/dist/apps/workspace-rekey.js +35 -0
- package/dist/apps/workspace-rekey.test.d.ts +1 -0
- package/dist/apps/workspace-rekey.test.js +23 -0
- package/dist/assets/iconIds.generated.d.ts +2 -0
- package/dist/assets/iconIds.generated.js +154 -0
- package/dist/auth/admin-users.svelte.d.ts +9 -0
- package/dist/auth/admin-users.svelte.js +42 -0
- package/dist/auth/admin-users.test.d.ts +1 -0
- package/dist/auth/admin-users.test.js +52 -0
- package/dist/createShell.js +5 -5
- package/dist/documents/config.d.ts +5 -1
- package/dist/documents/config.js +16 -8
- package/dist/documents/index.d.ts +1 -1
- package/dist/documents/index.js +1 -1
- package/dist/host-entry.d.ts +1 -1
- package/dist/host-entry.js +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/host.js +9 -2
- package/dist/primitives/Button.svelte +50 -4
- package/dist/primitives/Button.svelte.d.ts +3 -1
- package/dist/primitives/Collapsible.svelte +110 -0
- package/dist/primitives/Collapsible.svelte.d.ts +14 -0
- package/dist/primitives/widgets/AppPicker.svelte +41 -0
- package/dist/primitives/widgets/AppPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/AppPicker.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/AppPicker.svelte.test.js +26 -0
- package/dist/primitives/widgets/AppPicker.test.d.ts +1 -0
- package/dist/primitives/widgets/AppPicker.test.js +74 -0
- package/dist/primitives/widgets/ColorSwatch.svelte +7 -2
- package/dist/primitives/widgets/ColorSwatch.svelte.d.ts +2 -1
- package/dist/primitives/widgets/ColorSwatch.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/ColorSwatch.svelte.test.js +31 -0
- package/dist/primitives/widgets/Field.svelte +4 -2
- package/dist/primitives/widgets/Field.svelte.d.ts +2 -2
- package/dist/primitives/widgets/Field.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/Field.svelte.test.js +33 -0
- package/dist/primitives/widgets/FilePicker.svelte +2 -2
- package/dist/primitives/widgets/FilePicker.svelte.d.ts +2 -2
- package/dist/primitives/widgets/FilePicker.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/FilePicker.svelte.test.js +31 -0
- package/dist/primitives/widgets/IconPicker.svelte +115 -0
- package/dist/primitives/widgets/IconPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/IconPicker.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/IconPicker.svelte.test.js +43 -0
- package/dist/primitives/widgets/IconToggleGroup.svelte +4 -4
- package/dist/primitives/widgets/IconToggleGroup.svelte.d.ts +3 -3
- package/dist/primitives/widgets/IconToggleGroup.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/IconToggleGroup.svelte.test.js +40 -0
- package/dist/primitives/widgets/NumberInput.svelte +19 -9
- package/dist/primitives/widgets/NumberInput.svelte.d.ts +2 -2
- package/dist/primitives/widgets/NumberInput.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/NumberInput.svelte.test.js +48 -0
- package/dist/primitives/widgets/PickerList.d.ts +24 -0
- package/dist/primitives/widgets/PickerList.js +21 -0
- package/dist/primitives/widgets/PickerList.svelte +150 -0
- package/dist/primitives/widgets/PickerList.svelte.d.ts +16 -0
- package/dist/primitives/widgets/PickerList.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/PickerList.svelte.test.js +31 -0
- package/dist/primitives/widgets/PickerList.test.d.ts +1 -0
- package/dist/primitives/widgets/PickerList.test.js +218 -0
- package/dist/primitives/widgets/RangeSlider.svelte +11 -4
- package/dist/primitives/widgets/RangeSlider.svelte.d.ts +2 -2
- package/dist/primitives/widgets/RangeSlider.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/RangeSlider.svelte.test.js +38 -0
- package/dist/primitives/widgets/Segmented.svelte +4 -4
- package/dist/primitives/widgets/Segmented.svelte.d.ts +3 -3
- package/dist/primitives/widgets/Segmented.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/Segmented.svelte.test.js +25 -0
- package/dist/primitives/widgets/Select.svelte +4 -4
- package/dist/primitives/widgets/Select.svelte.d.ts +3 -3
- package/dist/primitives/widgets/Select.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/Select.svelte.test.js +37 -0
- package/dist/primitives/widgets/Slider.svelte +4 -2
- package/dist/primitives/widgets/Slider.svelte.d.ts +2 -2
- package/dist/primitives/widgets/Slider.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/Slider.svelte.test.js +22 -0
- package/dist/primitives/widgets/SliderGroup.svelte +4 -2
- package/dist/primitives/widgets/SliderGroup.svelte.d.ts +2 -2
- package/dist/primitives/widgets/SliderGroup.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/SliderGroup.svelte.test.js +34 -0
- package/dist/primitives/widgets/Textarea.svelte +5 -2
- package/dist/primitives/widgets/Textarea.svelte.d.ts +2 -2
- package/dist/primitives/widgets/Textarea.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/Textarea.svelte.test.js +29 -0
- package/dist/primitives/widgets/UserPicker.svelte +53 -0
- package/dist/primitives/widgets/UserPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/UserPicker.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/UserPicker.svelte.test.js +30 -0
- package/dist/primitives/widgets/UserPicker.test.d.ts +1 -0
- package/dist/primitives/widgets/UserPicker.test.js +115 -0
- package/dist/primitives/widgets/_contract.d.ts +27 -0
- package/dist/primitives/widgets/_contract.js +10 -0
- package/dist/projects/session-state.svelte.d.ts +17 -0
- package/dist/projects/session-state.svelte.js +39 -0
- package/dist/projects/session-state.test.d.ts +1 -0
- package/dist/projects/session-state.test.js +55 -0
- package/dist/projects-shard/DeleteProjectDialog.svelte +150 -0
- package/dist/projects-shard/DeleteProjectDialog.svelte.d.ts +12 -0
- package/dist/projects-shard/DeleteProjectDialog.test.d.ts +1 -0
- package/dist/projects-shard/DeleteProjectDialog.test.js +120 -0
- package/dist/projects-shard/ProjectManage.svelte +219 -0
- package/dist/projects-shard/ProjectManage.svelte.d.ts +8 -0
- package/dist/projects-shard/ProjectsSection.svelte +120 -0
- package/dist/projects-shard/ProjectsSection.svelte.d.ts +3 -0
- package/dist/projects-shard/index.d.ts +4 -0
- package/dist/projects-shard/index.js +4 -0
- package/dist/projects-shard/projectsApi.d.ts +20 -0
- package/dist/projects-shard/projectsApi.js +44 -0
- package/dist/projects-shard/projectsApi.test.d.ts +1 -0
- package/dist/projects-shard/projectsApi.test.js +71 -0
- package/dist/projects-shard/projectsShard.svelte.d.ts +10 -0
- package/dist/projects-shard/projectsShard.svelte.js +148 -0
- package/dist/sh3core-shard/ShellHome.svelte +83 -39
- package/dist/sh3core-shard/appActions.d.ts +13 -0
- package/dist/sh3core-shard/appActions.js +181 -0
- package/dist/sh3core-shard/appActions.test.d.ts +1 -0
- package/dist/sh3core-shard/appActions.test.js +25 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -0
- package/dist/shards/activate-scopeid.test.d.ts +1 -0
- package/dist/shards/{activate-tenantid.test.js → activate-scopeid.test.js} +6 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- /package/dist/{shards/activate-tenantid.test.d.ts → actions/resolveLabel.test.d.ts} +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Client wrapper for the /api/projects HTTP surface.
|
|
3
|
+
*
|
|
4
|
+
* Returns plain JSON objects; the shard layer is responsible for keeping
|
|
5
|
+
* a reactive copy and for converting these to actions / views.
|
|
6
|
+
*/
|
|
7
|
+
async function jsonFetch(url, init) {
|
|
8
|
+
var _a;
|
|
9
|
+
const res = await fetch(url, Object.assign({ credentials: 'include' }, init));
|
|
10
|
+
if (!res.ok)
|
|
11
|
+
throw new Error(`${(_a = init === null || init === void 0 ? void 0 : init.method) !== null && _a !== void 0 ? _a : 'GET'} ${url} failed: ${res.status}`);
|
|
12
|
+
return res.json();
|
|
13
|
+
}
|
|
14
|
+
export const projectsApi = {
|
|
15
|
+
async list() {
|
|
16
|
+
const body = await jsonFetch('/api/projects', { method: 'GET' });
|
|
17
|
+
return body.projects;
|
|
18
|
+
},
|
|
19
|
+
async listAll() {
|
|
20
|
+
const body = await jsonFetch('/api/projects/all', { method: 'GET' });
|
|
21
|
+
return body.projects;
|
|
22
|
+
},
|
|
23
|
+
async get(id) {
|
|
24
|
+
return jsonFetch(`/api/projects/${encodeURIComponent(id)}`, { method: 'GET' });
|
|
25
|
+
},
|
|
26
|
+
async create(input) {
|
|
27
|
+
return jsonFetch('/api/projects', {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'content-type': 'application/json' },
|
|
30
|
+
body: JSON.stringify(input),
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
async update(id, patch) {
|
|
34
|
+
return jsonFetch(`/api/projects/${encodeURIComponent(id)}`, {
|
|
35
|
+
method: 'PATCH',
|
|
36
|
+
headers: { 'content-type': 'application/json' },
|
|
37
|
+
body: JSON.stringify(patch),
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
async delete(id, opts) {
|
|
41
|
+
const qs = (opts === null || opts === void 0 ? void 0 : opts.wipeData) ? '?wipeData=1' : '';
|
|
42
|
+
await jsonFetch(`/api/projects/${encodeURIComponent(id)}${qs}`, { method: 'DELETE' });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { projectsApi } from './projectsApi';
|
|
3
|
+
describe('projectsApi', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
6
|
+
});
|
|
7
|
+
it('list() GETs /api/projects and returns projects array', async () => {
|
|
8
|
+
globalThis.fetch.mockResolvedValue({
|
|
9
|
+
ok: true,
|
|
10
|
+
json: async () => ({ projects: [{ id: 'acme-abcd', name: 'Acme', members: [], appAllowlist: [], createdBy: 'admin', createdAt: 0, updatedAt: 0 }] }),
|
|
11
|
+
});
|
|
12
|
+
const projects = await projectsApi.list();
|
|
13
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/projects', expect.objectContaining({ method: 'GET' }));
|
|
14
|
+
expect(projects).toHaveLength(1);
|
|
15
|
+
expect(projects[0].name).toBe('Acme');
|
|
16
|
+
});
|
|
17
|
+
it('listAll() GETs /api/projects/all', async () => {
|
|
18
|
+
globalThis.fetch.mockResolvedValue({
|
|
19
|
+
ok: true,
|
|
20
|
+
json: async () => ({ projects: [] }),
|
|
21
|
+
});
|
|
22
|
+
await projectsApi.listAll();
|
|
23
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/projects/all', expect.objectContaining({ method: 'GET' }));
|
|
24
|
+
});
|
|
25
|
+
it('create() POSTs JSON body to /api/projects', async () => {
|
|
26
|
+
globalThis.fetch.mockResolvedValue({
|
|
27
|
+
ok: true,
|
|
28
|
+
json: async () => ({ id: 'x-1234', name: 'X', members: [], appAllowlist: [], createdBy: 'admin', createdAt: 0, updatedAt: 0 }),
|
|
29
|
+
});
|
|
30
|
+
const project = await projectsApi.create({ name: 'X', members: [], appAllowlist: [] });
|
|
31
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/projects', expect.objectContaining({ method: 'POST' }));
|
|
32
|
+
expect(project.id).toBe('x-1234');
|
|
33
|
+
});
|
|
34
|
+
it('update() PATCHes /api/projects/:id', async () => {
|
|
35
|
+
globalThis.fetch.mockResolvedValue({
|
|
36
|
+
ok: true,
|
|
37
|
+
json: async () => ({ id: 'a', name: 'A', members: [], appAllowlist: [], createdBy: 'admin', createdAt: 0, updatedAt: 0 }),
|
|
38
|
+
});
|
|
39
|
+
await projectsApi.update('a', { name: 'A' });
|
|
40
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/projects/a', expect.objectContaining({ method: 'PATCH' }));
|
|
41
|
+
});
|
|
42
|
+
it('delete() DELETEs /api/projects/:id', async () => {
|
|
43
|
+
globalThis.fetch.mockResolvedValue({ ok: true, json: async () => ({ ok: true }) });
|
|
44
|
+
await projectsApi.delete('a');
|
|
45
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/projects/a', expect.objectContaining({ method: 'DELETE' }));
|
|
46
|
+
});
|
|
47
|
+
it('throws on non-ok response', async () => {
|
|
48
|
+
globalThis.fetch.mockResolvedValue({ ok: false, status: 403 });
|
|
49
|
+
await expect(projectsApi.list()).rejects.toThrow(/403/);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('projectsApi.delete with wipeData', () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
55
|
+
});
|
|
56
|
+
it('appends ?wipeData=1 when opts.wipeData is true', async () => {
|
|
57
|
+
globalThis.fetch.mockResolvedValue({ ok: true, json: async () => ({ ok: true, wipedData: true }) });
|
|
58
|
+
await projectsApi.delete('a', { wipeData: true });
|
|
59
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/projects/a?wipeData=1', expect.objectContaining({ method: 'DELETE' }));
|
|
60
|
+
});
|
|
61
|
+
it('does not append the query when opts is omitted', async () => {
|
|
62
|
+
globalThis.fetch.mockResolvedValue({ ok: true, json: async () => ({ ok: true, wipedData: false }) });
|
|
63
|
+
await projectsApi.delete('a');
|
|
64
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/projects/a', expect.objectContaining({ method: 'DELETE' }));
|
|
65
|
+
});
|
|
66
|
+
it('does not append the query when opts.wipeData is false', async () => {
|
|
67
|
+
globalThis.fetch.mockResolvedValue({ ok: true, json: async () => ({ ok: true, wipedData: false }) });
|
|
68
|
+
await projectsApi.delete('a', { wipeData: false });
|
|
69
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/projects/a', expect.objectContaining({ method: 'DELETE' }));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Shard } from '../shards/types';
|
|
2
|
+
import { type ProjectRecord } from './projectsApi';
|
|
3
|
+
export declare const projectsState: {
|
|
4
|
+
projects: ProjectRecord[];
|
|
5
|
+
loading: boolean;
|
|
6
|
+
error: string | null;
|
|
7
|
+
};
|
|
8
|
+
export declare function refreshProjects(): Promise<void>;
|
|
9
|
+
export declare function openProjectManage(project: ProjectRecord | null): void;
|
|
10
|
+
export declare const projectsShard: Shard;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `__projects__` shard — multi-member project scope module.
|
|
3
|
+
*
|
|
4
|
+
* Maintains a reactive list of projects the current user is a member of.
|
|
5
|
+
* Refreshes on activate and on visibility-change so a project created in
|
|
6
|
+
* one tab appears in another after it regains focus. Registers admin-only
|
|
7
|
+
* actions that open the ProjectManage float view (create/edit/delete).
|
|
8
|
+
*/
|
|
9
|
+
import { mount, unmount } from 'svelte';
|
|
10
|
+
import { projectsApi } from './projectsApi';
|
|
11
|
+
import { VERSION } from '../version';
|
|
12
|
+
import ProjectManage from './ProjectManage.svelte';
|
|
13
|
+
import { floatManager } from '../overlays/float';
|
|
14
|
+
import { isAdmin } from '../auth/auth.svelte';
|
|
15
|
+
export const projectsState = $state({ projects: [], loading: false, error: null });
|
|
16
|
+
export async function refreshProjects() {
|
|
17
|
+
projectsState.loading = true;
|
|
18
|
+
projectsState.error = null;
|
|
19
|
+
try {
|
|
20
|
+
projectsState.projects = await projectsApi.list();
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
projectsState.error = e.message;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
projectsState.loading = false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const PROJECTS_MANAGE_VIEW = 'projects:manage';
|
|
30
|
+
/*
|
|
31
|
+
* The float manager drops `meta` for `dismissable: true` (per the bare
|
|
32
|
+
* SlotNode wrapper in float.ts). Edit-mode needs to know which project
|
|
33
|
+
* to load, so we stash the target on a module-level slot the factory
|
|
34
|
+
* consumes on mount. Cleared after the factory has read it so a later
|
|
35
|
+
* Create from the palette can't accidentally inherit a previous edit
|
|
36
|
+
* target.
|
|
37
|
+
*/
|
|
38
|
+
let pendingTarget = null;
|
|
39
|
+
function consumePendingTarget() {
|
|
40
|
+
const t = pendingTarget;
|
|
41
|
+
pendingTarget = null;
|
|
42
|
+
return t;
|
|
43
|
+
}
|
|
44
|
+
export function openProjectManage(project) {
|
|
45
|
+
pendingTarget = project;
|
|
46
|
+
floatManager.open(PROJECTS_MANAGE_VIEW, {
|
|
47
|
+
title: project ? `Edit ${project.name}` : 'Create Project',
|
|
48
|
+
size: { w: 560, h: 620 },
|
|
49
|
+
dismissable: true,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export const projectsShard = {
|
|
53
|
+
manifest: {
|
|
54
|
+
id: '__projects__',
|
|
55
|
+
label: 'Projects',
|
|
56
|
+
version: VERSION,
|
|
57
|
+
views: [{ id: PROJECTS_MANAGE_VIEW, label: 'Project Manager' }],
|
|
58
|
+
},
|
|
59
|
+
activate(ctx) {
|
|
60
|
+
void refreshProjects();
|
|
61
|
+
if (typeof document !== 'undefined') {
|
|
62
|
+
document.addEventListener('visibilitychange', () => {
|
|
63
|
+
if (document.visibilityState === 'visible')
|
|
64
|
+
void refreshProjects();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const factory = {
|
|
68
|
+
mount(container, _mountCtx) {
|
|
69
|
+
const project = consumePendingTarget();
|
|
70
|
+
// The float manager has already pushed an entry for this mount —
|
|
71
|
+
// the most recent one whose viewId matches us is ours.
|
|
72
|
+
const list = floatManager.list();
|
|
73
|
+
const floatId = list.length > 0 ? list[list.length - 1].id : null;
|
|
74
|
+
const close = () => {
|
|
75
|
+
if (floatId)
|
|
76
|
+
floatManager.close(floatId);
|
|
77
|
+
};
|
|
78
|
+
const instance = mount(ProjectManage, {
|
|
79
|
+
target: container,
|
|
80
|
+
props: { project, onClose: close },
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
unmount() {
|
|
84
|
+
unmount(instance);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
ctx.registerView(PROJECTS_MANAGE_VIEW, factory);
|
|
90
|
+
ctx.actions.register({
|
|
91
|
+
id: 'sh3.project.create',
|
|
92
|
+
label: 'Create Project…',
|
|
93
|
+
scope: ['home', 'app'],
|
|
94
|
+
paletteItem: true,
|
|
95
|
+
disabled: () => !isAdmin(),
|
|
96
|
+
run: () => {
|
|
97
|
+
if (isAdmin())
|
|
98
|
+
openProjectManage(null);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
ctx.actions.register({
|
|
102
|
+
id: 'sh3.project.manage',
|
|
103
|
+
label: 'Manage Project…',
|
|
104
|
+
scope: ['home', 'app'],
|
|
105
|
+
submenu: true,
|
|
106
|
+
paletteItem: true,
|
|
107
|
+
disabled: () => !isAdmin(),
|
|
108
|
+
});
|
|
109
|
+
// Dynamic children: one per project in projectsState.projects, kept in
|
|
110
|
+
// sync as projects are created/deleted. Mirrors the sh3.app.launch
|
|
111
|
+
// pattern in sh3coreShard.
|
|
112
|
+
const manageUnregisters = new Map();
|
|
113
|
+
$effect.root(() => {
|
|
114
|
+
$effect(() => {
|
|
115
|
+
const currentIds = new Set();
|
|
116
|
+
for (const project of projectsState.projects) {
|
|
117
|
+
currentIds.add(project.id);
|
|
118
|
+
if (manageUnregisters.has(project.id))
|
|
119
|
+
continue;
|
|
120
|
+
const off = ctx.actions.register({
|
|
121
|
+
id: `sh3.project.manage:${project.id}`,
|
|
122
|
+
label: project.name,
|
|
123
|
+
scope: ['home', 'app'],
|
|
124
|
+
submenuOf: 'sh3.project.manage',
|
|
125
|
+
run: () => {
|
|
126
|
+
// Re-read the live record so renames since registration are
|
|
127
|
+
// reflected in the form.
|
|
128
|
+
const live = projectsState.projects.find((p) => p.id === project.id);
|
|
129
|
+
if (live)
|
|
130
|
+
openProjectManage(live);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
manageUnregisters.set(project.id, off);
|
|
134
|
+
}
|
|
135
|
+
for (const id of [...manageUnregisters.keys()]) {
|
|
136
|
+
if (!currentIds.has(id)) {
|
|
137
|
+
manageUnregisters.get(id)();
|
|
138
|
+
manageUnregisters.delete(id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
autostart() {
|
|
145
|
+
/* register on the self-starting path so the project list is available
|
|
146
|
+
on the home screen without an app launch. */
|
|
147
|
+
},
|
|
148
|
+
};
|
|
@@ -9,19 +9,51 @@
|
|
|
9
9
|
|
|
10
10
|
import { listRegisteredApps, launchApp, isAdmin, VERSION } from '../api';
|
|
11
11
|
import ShellTitle from './ShellTitle.svelte';
|
|
12
|
+
import ProjectsSection from '../projects-shard/ProjectsSection.svelte';
|
|
13
|
+
import { sessionState } from '../projects/session-state.svelte';
|
|
14
|
+
import { projectsState } from '../projects-shard/projectsShard.svelte';
|
|
15
|
+
import { shell } from '../shellRuntime.svelte';
|
|
16
|
+
import { makeSelectionApi } from '../actions/selection.svelte';
|
|
17
|
+
import { getAppearance } from '../app-appearance';
|
|
18
|
+
import iconsUrl from '../assets/icons.svg';
|
|
19
|
+
|
|
20
|
+
const homeSelection = makeSelectionApi('__sh3core__');
|
|
21
|
+
|
|
22
|
+
function openAppContextMenu(event: MouseEvent, appId: string): void {
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
homeSelection.set({ type: 'app', ref: { appId } });
|
|
25
|
+
shell.actions.openContextMenu({
|
|
26
|
+
x: event.clientX,
|
|
27
|
+
y: event.clientY,
|
|
28
|
+
scope: { element: 'app' },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
12
31
|
|
|
13
32
|
let filter = $state('');
|
|
14
33
|
|
|
15
34
|
const apps = $derived(listRegisteredApps());
|
|
16
35
|
const elevated = $derived(isAdmin());
|
|
17
36
|
|
|
37
|
+
const activeProject = $derived(
|
|
38
|
+
sessionState.activeProjectId
|
|
39
|
+
? projectsState.projects.find((p) => p.id === sessionState.activeProjectId) ?? null
|
|
40
|
+
: null,
|
|
41
|
+
);
|
|
42
|
+
|
|
18
43
|
function matches(m: { id: string; label: string }, q: string): boolean {
|
|
19
44
|
if (!q) return true;
|
|
20
45
|
const needle = q.toLowerCase();
|
|
21
46
|
return m.label.toLowerCase().includes(needle) || m.id.toLowerCase().includes(needle);
|
|
22
47
|
}
|
|
23
48
|
|
|
24
|
-
|
|
49
|
+
function inAllowlist(appId: string): boolean {
|
|
50
|
+
if (!activeProject) return true;
|
|
51
|
+
return activeProject.appAllowlist.includes(appId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const userApps = $derived(
|
|
55
|
+
apps.filter((m) => !m.admin && matches(m, filter) && inAllowlist(m.id)),
|
|
56
|
+
);
|
|
25
57
|
const adminApps = $derived(apps.filter((m) => m.admin && matches(m, filter)));
|
|
26
58
|
const totalVisible = $derived(userApps.length + (elevated ? adminApps.length : 0));
|
|
27
59
|
</script>
|
|
@@ -48,22 +80,27 @@
|
|
|
48
80
|
/>
|
|
49
81
|
</div>
|
|
50
82
|
|
|
83
|
+
<ProjectsSection />
|
|
84
|
+
|
|
51
85
|
{#if userApps.length > 0}
|
|
52
86
|
<section class="shell-home-section">
|
|
53
87
|
<h2 class="shell-home-section-title">Apps</h2>
|
|
54
88
|
<div class="shell-home-grid">
|
|
55
89
|
{#each userApps as manifest (manifest.id)}
|
|
90
|
+
{@const appearance = getAppearance(manifest.id)}
|
|
56
91
|
<button
|
|
57
92
|
type="button"
|
|
58
93
|
class="shell-home-card"
|
|
94
|
+
class:shell-home-card--tinted={appearance?.color}
|
|
95
|
+
style:--card-color={appearance?.color ?? 'transparent'}
|
|
96
|
+
data-sh3-scope="element:app"
|
|
59
97
|
onclick={() => launchApp(manifest.id)}
|
|
60
|
-
|
|
98
|
+
oncontextmenu={(e) => openAppContextMenu(e, manifest.id)}
|
|
61
99
|
>
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
</div>
|
|
100
|
+
<span class="shell-home-card-square">
|
|
101
|
+
<svg class="shell-home-card-icon"><use href="{iconsUrl}#{appearance?.icon ?? manifest.icon ?? 'box'}" /></svg>
|
|
102
|
+
</span>
|
|
103
|
+
<span class="shell-home-card-label">{appearance?.label ?? manifest.label}</span>
|
|
67
104
|
</button>
|
|
68
105
|
{/each}
|
|
69
106
|
</div>
|
|
@@ -75,17 +112,20 @@
|
|
|
75
112
|
<h2 class="shell-home-section-title">Admin</h2>
|
|
76
113
|
<div class="shell-home-grid">
|
|
77
114
|
{#each adminApps as manifest (manifest.id)}
|
|
115
|
+
{@const appearance = getAppearance(manifest.id)}
|
|
78
116
|
<button
|
|
79
117
|
type="button"
|
|
80
118
|
class="shell-home-card"
|
|
119
|
+
class:shell-home-card--tinted={appearance?.color}
|
|
120
|
+
style:--card-color={appearance?.color ?? 'transparent'}
|
|
121
|
+
data-sh3-scope="element:app"
|
|
81
122
|
onclick={() => launchApp(manifest.id)}
|
|
82
|
-
|
|
123
|
+
oncontextmenu={(e) => openAppContextMenu(e, manifest.id)}
|
|
83
124
|
>
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
</div>
|
|
125
|
+
<span class="shell-home-card-square">
|
|
126
|
+
<svg class="shell-home-card-icon"><use href="{iconsUrl}#{appearance?.icon ?? manifest.icon ?? 'box'}" /></svg>
|
|
127
|
+
</span>
|
|
128
|
+
<span class="shell-home-card-label">{appearance?.label ?? manifest.label}</span>
|
|
89
129
|
</button>
|
|
90
130
|
{/each}
|
|
91
131
|
</div>
|
|
@@ -205,26 +245,34 @@
|
|
|
205
245
|
}
|
|
206
246
|
.shell-home-grid {
|
|
207
247
|
display: grid;
|
|
208
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
209
|
-
gap:
|
|
248
|
+
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
|
|
249
|
+
gap: 18px 14px;
|
|
210
250
|
}
|
|
211
251
|
.shell-home-card {
|
|
212
|
-
aspect-ratio: 1 / 1;
|
|
213
252
|
display: flex;
|
|
214
253
|
flex-direction: column;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
padding:
|
|
218
|
-
background:
|
|
219
|
-
border:
|
|
220
|
-
border-radius: var(--shell-radius-md);
|
|
254
|
+
align-items: center;
|
|
255
|
+
gap: 6px;
|
|
256
|
+
padding: 0;
|
|
257
|
+
background: transparent;
|
|
258
|
+
border: none;
|
|
221
259
|
color: inherit;
|
|
222
260
|
font: inherit;
|
|
223
261
|
cursor: pointer;
|
|
262
|
+
}
|
|
263
|
+
.shell-home-card-square {
|
|
264
|
+
width: 64px;
|
|
265
|
+
height: 64px;
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
justify-content: center;
|
|
269
|
+
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
|
|
270
|
+
border: 1px solid var(--shell-border);
|
|
271
|
+
border-radius: var(--shell-radius-md);
|
|
224
272
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.15);
|
|
225
273
|
transition: transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
|
|
226
274
|
}
|
|
227
|
-
.shell-home-card:hover {
|
|
275
|
+
.shell-home-card:hover .shell-home-card-square {
|
|
228
276
|
border-color: var(--shell-accent);
|
|
229
277
|
transform: translateY(-1px);
|
|
230
278
|
box-shadow:
|
|
@@ -234,36 +282,32 @@
|
|
|
234
282
|
}
|
|
235
283
|
.shell-home-card:focus-visible {
|
|
236
284
|
outline: none;
|
|
285
|
+
}
|
|
286
|
+
.shell-home-card:focus-visible .shell-home-card-square {
|
|
237
287
|
border-color: var(--shell-accent);
|
|
238
288
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--shell-accent) 40%, transparent);
|
|
239
289
|
}
|
|
240
|
-
.shell-home-card:active {
|
|
290
|
+
.shell-home-card:active .shell-home-card-square {
|
|
241
291
|
transform: translateY(0);
|
|
242
292
|
}
|
|
243
293
|
.shell-home-card-label {
|
|
244
294
|
font-weight: 600;
|
|
245
|
-
font-size:
|
|
295
|
+
font-size: 11px;
|
|
246
296
|
line-height: 1.2;
|
|
297
|
+
text-align: center;
|
|
247
298
|
overflow: hidden;
|
|
248
299
|
display: -webkit-box;
|
|
249
300
|
-webkit-box-orient: vertical;
|
|
250
301
|
-webkit-line-clamp: 2;
|
|
251
302
|
line-clamp: 2;
|
|
303
|
+
word-break: break-word;
|
|
252
304
|
}
|
|
253
|
-
.shell-home-card-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
font-size: 9px;
|
|
258
|
-
color: var(--shell-fg-subtle);
|
|
259
|
-
min-width: 0;
|
|
260
|
-
}
|
|
261
|
-
.shell-home-card-id {
|
|
262
|
-
overflow: hidden;
|
|
263
|
-
text-overflow: ellipsis;
|
|
264
|
-
white-space: nowrap;
|
|
305
|
+
.shell-home-card-icon {
|
|
306
|
+
width: 28px;
|
|
307
|
+
height: 28px;
|
|
308
|
+
color: var(--shell-fg);
|
|
265
309
|
}
|
|
266
|
-
.shell-home-card-
|
|
267
|
-
|
|
310
|
+
.shell-home-card--tinted .shell-home-card-square {
|
|
311
|
+
background: var(--card-color);
|
|
268
312
|
}
|
|
269
313
|
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ShardContext } from '../shards/types';
|
|
2
|
+
export interface AppActionGate {
|
|
3
|
+
admin: boolean;
|
|
4
|
+
builtin: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function computeAppActionDisabled(g: AppActionGate): boolean;
|
|
7
|
+
export declare function computeAppActionLabelSuffix(g: AppActionGate): string;
|
|
8
|
+
/**
|
|
9
|
+
* Register the three element-scope app actions on the given shard context.
|
|
10
|
+
* Returns a disposer that unregisters all three (the framework expects the
|
|
11
|
+
* shard's activate to keep the disposers alive across deactivate).
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerAppActions(ctx: ShardContext): () => void;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Element-scope actions for app cards. Wired by sh3coreShard.activate.
|
|
3
|
+
* The element scope is { element: 'app' } and activates when right-click
|
|
4
|
+
* on a host with data-sh3-scope="element:app" sets a selection of type
|
|
5
|
+
* 'app' (see ShellHome.svelte).
|
|
6
|
+
*
|
|
7
|
+
* Three actions are registered here:
|
|
8
|
+
* - app.info : info-only row showing "<id> v<version>" (always disabled).
|
|
9
|
+
* - app.checkUpdate : refresh catalog, then prompt update or toast up-to-date.
|
|
10
|
+
* - app.uninstall : open uninstall confirm dialog.
|
|
11
|
+
*
|
|
12
|
+
* The fourth menu item (app.customize) is registered by the app-appearance
|
|
13
|
+
* shard so its lifecycle is bound to that shard's activate/deactivate.
|
|
14
|
+
*
|
|
15
|
+
* Disabled predicate: !isAdmin || isBuiltin. Built-in means the app id is
|
|
16
|
+
* NOT in storeContext.state.ephemeral.installed (those are framework-shipped
|
|
17
|
+
* shards like sh3-store, sh3-shell, projects, etc.).
|
|
18
|
+
*/
|
|
19
|
+
import { listRegisteredApps } from '../api';
|
|
20
|
+
import { isAdmin } from '../auth/auth.svelte';
|
|
21
|
+
import { getSelection } from '../actions/selection.svelte';
|
|
22
|
+
import { storeContext } from '../app/store/storeShard.svelte';
|
|
23
|
+
import { openPermissionConfirmModal } from '../app/store/permissionConfirm';
|
|
24
|
+
import { modalManager } from '../overlays/modal';
|
|
25
|
+
import { toastManager } from '../overlays/toast';
|
|
26
|
+
import AppUpdateAvailableModal from '../app/store/AppUpdateAvailableModal.svelte';
|
|
27
|
+
import UninstallAppDialog from '../app/store/UninstallAppDialog.svelte';
|
|
28
|
+
export function computeAppActionDisabled(g) {
|
|
29
|
+
return !g.admin || g.builtin;
|
|
30
|
+
}
|
|
31
|
+
export function computeAppActionLabelSuffix(g) {
|
|
32
|
+
if (!g.admin)
|
|
33
|
+
return ' · admin only';
|
|
34
|
+
if (g.builtin)
|
|
35
|
+
return ' · built-in';
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
function readSelection() {
|
|
39
|
+
const sel = getSelection();
|
|
40
|
+
if (!sel || sel.type !== 'app')
|
|
41
|
+
return null;
|
|
42
|
+
return sel.ref;
|
|
43
|
+
}
|
|
44
|
+
function findApp(appId) {
|
|
45
|
+
return listRegisteredApps().find((m) => m.id === appId);
|
|
46
|
+
}
|
|
47
|
+
function isBuiltin(appId) {
|
|
48
|
+
return !storeContext.state.ephemeral.installed.some((p) => p.id === appId);
|
|
49
|
+
}
|
|
50
|
+
function gateFor(appId) {
|
|
51
|
+
return { admin: isAdmin(), builtin: isBuiltin(appId) };
|
|
52
|
+
}
|
|
53
|
+
async function runCheckUpdate(_ctx) {
|
|
54
|
+
var _a, _b;
|
|
55
|
+
const ref = readSelection();
|
|
56
|
+
if (!ref)
|
|
57
|
+
return;
|
|
58
|
+
const { appId } = ref;
|
|
59
|
+
const checking = toastManager.notify('Checking for updates…', { duration: Infinity });
|
|
60
|
+
try {
|
|
61
|
+
await storeContext.refreshCatalog();
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
checking.close();
|
|
65
|
+
}
|
|
66
|
+
const entry = storeContext.state.ephemeral.updatable[appId];
|
|
67
|
+
const installed = storeContext.state.ephemeral.installed.find((p) => p.id === appId);
|
|
68
|
+
const label = (_b = (_a = findApp(appId)) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : appId;
|
|
69
|
+
if (!entry || !installed) {
|
|
70
|
+
toastManager.notify(`${label} is up to date`, { level: 'info', duration: 2500 });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const props = {
|
|
74
|
+
appId,
|
|
75
|
+
appLabel: label,
|
|
76
|
+
fromVersion: installed.version,
|
|
77
|
+
toVersion: entry.latest.version,
|
|
78
|
+
onConfirm: async () => {
|
|
79
|
+
try {
|
|
80
|
+
await storeContext.updatePackage(appId, (added, removed) => openPermissionConfirmModal({ label: installed.id, version: installed.version }, entry.latest.version, added, removed));
|
|
81
|
+
toastManager.notify(`Updated to v${entry.latest.version}`, { level: 'success' });
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
toastManager.notify(`Update failed: ${e.message}`, { level: 'error', duration: 5000 });
|
|
85
|
+
throw e;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
modalManager.open(AppUpdateAvailableModal, props);
|
|
90
|
+
}
|
|
91
|
+
function runUninstall(_ctx) {
|
|
92
|
+
var _a, _b;
|
|
93
|
+
const ref = readSelection();
|
|
94
|
+
if (!ref)
|
|
95
|
+
return;
|
|
96
|
+
const { appId } = ref;
|
|
97
|
+
const installed = storeContext.state.ephemeral.installed.find((p) => p.id === appId);
|
|
98
|
+
if (!installed)
|
|
99
|
+
return;
|
|
100
|
+
const label = (_b = (_a = findApp(appId)) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : appId;
|
|
101
|
+
const props = {
|
|
102
|
+
appId,
|
|
103
|
+
appLabel: label,
|
|
104
|
+
version: installed.version,
|
|
105
|
+
onConfirm: async () => {
|
|
106
|
+
try {
|
|
107
|
+
await storeContext.uninstallPackage(appId);
|
|
108
|
+
toastManager.notify(`Uninstalled ${label}`, { level: 'success' });
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
toastManager.notify(`Uninstall failed: ${e.message}`, { level: 'error', duration: 5000 });
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
modalManager.open(UninstallAppDialog, props);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Register the three element-scope app actions on the given shard context.
|
|
120
|
+
* Returns a disposer that unregisters all three (the framework expects the
|
|
121
|
+
* shard's activate to keep the disposers alive across deactivate).
|
|
122
|
+
*/
|
|
123
|
+
export function registerAppActions(ctx) {
|
|
124
|
+
const infoLabel = () => {
|
|
125
|
+
const ref = readSelection();
|
|
126
|
+
if (!ref)
|
|
127
|
+
return '';
|
|
128
|
+
const m = findApp(ref.appId);
|
|
129
|
+
if (!m)
|
|
130
|
+
return ref.appId;
|
|
131
|
+
return `${m.id} v${m.version}`;
|
|
132
|
+
};
|
|
133
|
+
const updateLabel = () => {
|
|
134
|
+
const ref = readSelection();
|
|
135
|
+
if (!ref)
|
|
136
|
+
return 'Check for updates';
|
|
137
|
+
return `Check for updates${computeAppActionLabelSuffix(gateFor(ref.appId))}`;
|
|
138
|
+
};
|
|
139
|
+
const uninstallLabel = () => {
|
|
140
|
+
const ref = readSelection();
|
|
141
|
+
if (!ref)
|
|
142
|
+
return 'Uninstall…';
|
|
143
|
+
return `Uninstall…${computeAppActionLabelSuffix(gateFor(ref.appId))}`;
|
|
144
|
+
};
|
|
145
|
+
const isDisabledForCurrent = () => {
|
|
146
|
+
const ref = readSelection();
|
|
147
|
+
if (!ref)
|
|
148
|
+
return true;
|
|
149
|
+
return computeAppActionDisabled(gateFor(ref.appId));
|
|
150
|
+
};
|
|
151
|
+
const actions = [
|
|
152
|
+
{
|
|
153
|
+
id: 'app.info',
|
|
154
|
+
label: infoLabel,
|
|
155
|
+
scope: { element: 'app' },
|
|
156
|
+
contextItem: true,
|
|
157
|
+
group: 'info',
|
|
158
|
+
disabled: true,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'app.checkUpdate',
|
|
162
|
+
label: updateLabel,
|
|
163
|
+
scope: { element: 'app' },
|
|
164
|
+
contextItem: true,
|
|
165
|
+
group: 'update',
|
|
166
|
+
disabled: isDisabledForCurrent,
|
|
167
|
+
run: runCheckUpdate,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'app.uninstall',
|
|
171
|
+
label: uninstallLabel,
|
|
172
|
+
scope: { element: 'app' },
|
|
173
|
+
contextItem: true,
|
|
174
|
+
group: 'uninstall',
|
|
175
|
+
disabled: isDisabledForCurrent,
|
|
176
|
+
run: runUninstall,
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
const disposers = actions.map((a) => ctx.actions.register(a));
|
|
180
|
+
return () => disposers.forEach((d) => d());
|
|
181
|
+
}
|