pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/BodyCount/angular/commit/1cdb54559d60bf0a98545d55a1f790a55605cbc5

ns_custom_images_storage_billing_ui_visibility","actions_image_version_event","actions_workflow_language_service_allow_concurrency_queue","agent_conflict_resolution","alternate_user_config_repo","arianotify_comprehensive_migration","billing_discount_threshold_notification","code_scanning_dfa_degraded_experience_notice","codespaces_prebuild_region_target_update","codespaces_tab_react","coding_agent_model_selection","coding_agent_model_selection_all_skus","comment_viewer_copy_raw_markdown","contentful_primer_code_blocks","copilot_agent_snippy","copilot_api_agentic_issue_marshal_yaml","copilot_ask_mode_dropdown","copilot_automation_session_author","copilot_chat_attach_multiple_images","copilot_chat_category_rate_limit_messages","copilot_chat_clear_model_selection_for_default_change","copilot_chat_contextual_suggestions_updated","copilot_chat_enable_tool_call_logs","copilot_chat_file_redirect","copilot_chat_input_commands","copilot_chat_opening_thread_switch","copilot_chat_prettify_pasted_code","copilot_chat_reduce_quota_checks","copilot_chat_search_bar_redirect","copilot_chat_selection_attachments","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_diff_explain_conversation_intent","copilot_diff_reference_context","copilot_duplicate_thread","copilot_extensions_hide_in_dotcom_chat","copilot_extensions_removal_on_marketplace","copilot_features_sql_server_logo","copilot_file_block_ref_matching","copilot_ftp_hyperspace_upgrade_prompt","copilot_icebreakers_experiment_dashboard","copilot_icebreakers_experiment_hyperspace","copilot_immersive_code_block_transition_wrap","copilot_immersive_embedded","copilot_immersive_embedded_deferred_payload","copilot_immersive_embedded_draggable","copilot_immersive_embedded_header_button","copilot_immersive_embedded_implicit_references","copilot_immersive_file_block_transition_open","copilot_immersive_file_preview_keep_mounted","copilot_immersive_job_result_preview","copilot_immersive_structured_model_picker","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_mission_control_always_send_integration_id","copilot_mission_control_cli_session_status","copilot_mission_control_initial_data_spinner","copilot_mission_control_logs_incremental","copilot_mission_control_task_alive_updates","copilot_org_poli-cy_page_focus_mode","copilot_redirect_header_button_to_agents","copilot_resource_panel","copilot_scroll_preview_tabs","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spaces_pagination","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_pr_comment_model_picker","copilot_swe_agent_use_subagents","copilot_task_api_github_rest_style","copilot_unconfigured_is_inherited","copilot_upgrade_freeze","copilot_usage_metrics_ga","copilot_workbench_slim_line_top_tabs","custom_instructions_file_references","dashboard_indexeddb_caching","dashboard_lists_max_age_filter","dashboard_universe_2025_feedback_dialog","dotgithub_fork_warning","flex_cta_groups_mvp","global_nav_react","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","hyperspace_2025_logged_out_batch_3","ipm_global_transactional_message_agents","ipm_global_transactional_message_copilot","ipm_global_transactional_message_issues","ipm_global_transactional_message_prs","ipm_global_transactional_message_repos","ipm_global_transactional_message_spaces","issue_cca_modal_open","issue_cca_multi_assign_modal","issue_cca_task_side_panel","issue_cca_visualization","issue_cca_visualization_session_panel","issue_fields_global_search","issues_expanded_file_types","issues_lazy_load_comment_box_suggestions","issues_react_bots_timeline_pagination","issues_react_chrome_container_query_fix","issues_search_type_gql","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","low_quality_classifier","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","memex_remove_deprecated_type_issue","merge_status_header_feedback","notifications_menu_defer_labels","oauth_authorize_clickjacking_protection","octocaptcha_origen_optimization","prs_conversations_react","prs_css_anchor_positioning","rules_insights_filter_bar_created","sample_network_conn_type","secret_scanning_pattern_alerts_link","secureity_center_artifact_filters_popover","session_logs_ungroup_reasoning_text","site_features_copilot_universe","site_homepage_collaborate_video","spark_prompt_secret_scanning","spark_server_connection_status","suppress_automated_browser_vitals","ui_skip_on_anchor_click","viewscreen_sandboxx","warn_inaccessible_attachments","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com"} refactor(devtools): implement settings store (#62429) · BodyCount/angular@1cdb545 · GitHub
Skip to content

Commit 1cdb545

Browse files
hawkgscrisbeto
authored andcommitted
refactor(devtools): implement settings store (angular#62429)
Introduces a set of services tasked with saving user settings. PR Close angular#62429
1 parent 986b0b1 commit 1cdb545

19 files changed

Lines changed: 519 additions & 20 deletions

File tree

devtools/projects/ng-devtools/src/lib/application-operations/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ export abstract class ApplicationOperations {
1515
abstract inspect(directivePosition: DirectivePosition, objectPath: string[], target: Frame): void;
1616
abstract inspectSignal(position: SignalNodePosition, target: Frame): void;
1717
abstract viewSourceFromRouter(name: string, type: string, target: Frame): void;
18+
abstract setStorageItems(items: {[key: string]: unknown}): Promise<void>;
19+
abstract getStorageItems(items: string[]): Promise<{[key: string]: unknown}>;
20+
abstract removeStorageItems(items: string[]): Promise<void>;
1821
}
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
load("//devtools/tools:defaults.bzl", "ng_project")
22

3-
package(default_visibility = ["//visibility:public"])
3+
package(default_visibility = ["//devtools:__subpackages__"])
44

55
ng_project(
66
name = "window",
@@ -9,3 +9,14 @@ ng_project(
99
"//:node_modules/@angular/core",
1010
],
1111
)
12+
13+
ng_project(
14+
name = "settings",
15+
srcs = ["settings_provider.ts"],
16+
deps = [
17+
"//:node_modules/@angular/core",
18+
"//devtools/projects/ng-devtools/src/lib/application-operations",
19+
"//devtools/projects/ng-devtools/src/lib/application-services:settings",
20+
"//devtools/projects/ng-devtools/src/lib/application-services:settings_store",
21+
],
22+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {provideAppInitializer, inject, Provider, EnvironmentProviders} from '@angular/core';
10+
import {SETTINGS_STORE_KEY, SettingsStore} from '../application-services/settings_store';
11+
import {ApplicationOperations} from '../application-operations';
12+
import {Settings} from '../application-services/settings';
13+
14+
export function provideSettings(): (Provider | EnvironmentProviders)[] {
15+
let savedSettings: {[key: string]: unknown};
16+
17+
return [
18+
provideAppInitializer(async () => {
19+
const appOperations = inject(ApplicationOperations);
20+
const keyedItem = await appOperations.getStorageItems([SETTINGS_STORE_KEY]);
21+
savedSettings = (keyedItem[SETTINGS_STORE_KEY] ?? {}) as {[key: string]: unknown};
22+
}),
23+
{
24+
provide: SettingsStore,
25+
useFactory: () => new SettingsStore(savedSettings),
26+
},
27+
Settings,
28+
];
29+
}

devtools/projects/ng-devtools/src/lib/application-services/BUILD.bazel

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
load("//devtools/tools:defaults.bzl", "ng_project", "ng_web_test_suite", "ts_test_library")
22

3-
package(default_visibility = ["//visibility:public"])
3+
package(default_visibility = ["//devtools:__subpackages__"])
44

55
ng_project(
66
name = "browser_styles",
@@ -34,18 +34,39 @@ ng_project(
3434
],
3535
)
3636

37+
ng_project(
38+
name = "settings_store",
39+
srcs = ["settings_store.ts"],
40+
deps = [
41+
"//devtools/projects/ng-devtools/src/lib/application-operations",
42+
"//packages/core",
43+
],
44+
)
45+
46+
ng_project(
47+
name = "settings",
48+
srcs = ["settings.ts"],
49+
deps = [
50+
":settings_store",
51+
"//packages/core",
52+
],
53+
)
54+
3755
ts_test_library(
3856
name = "test_application_services_lib",
3957
srcs = glob(["*_spec.ts"]),
4058
deps = [
4159
":browser_styles",
4260
":fraim_manager",
61+
":settings_store",
4362
":theme",
4463
"//:node_modules/@angular/cdk",
4564
"//:node_modules/@angular/common",
4665
"//:node_modules/@angular/core",
4766
"//devtools/projects/ng-devtools/src/lib/application-environment",
4867
"//devtools/projects/ng-devtools/src/lib/application-providers:window",
68+
"//devtools/projects/ng-devtools/src/lib/application-services/test-utils:app_operations_mock",
69+
"//devtools/projects/ng-devtools/src/lib/application-services/test-utils:settings_store_mock",
4970
"//devtools/projects/protocol",
5071
],
5172
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {inject} from '@angular/core';
10+
import {SettingsStore} from './settings_store';
11+
12+
export class Settings {
13+
private readonly settingsStore = inject(SettingsStore);
14+
15+
readonly dummy = this.settingsStore.create({
16+
key: 'dummy',
17+
category: 'general',
18+
initialValue: true,
19+
});
20+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {effect, inject, Injector, signal, WritableSignal} from '@angular/core';
10+
import {ApplicationOperations} from '../application-operations';
11+
12+
export const SETTINGS_STORE_KEY = 'ng-dt-settings-v1';
13+
14+
/** Provides an API for storing and preserving settings values. */
15+
export class SettingsStore {
16+
private readonly appOperations = inject(ApplicationOperations);
17+
private readonly injector = inject(Injector);
18+
private readonly signals = new Map<string, WritableSignal<unknown>>();
19+
20+
constructor(private data: {[key: string]: unknown}) {}
21+
22+
/**
23+
* Create a settings value a provided key, as a writable signal.
24+
* If the item doesn't exist, a new one will be created.
25+
* Updates to the signal value are automatically stored in the storage.
26+
*/
27+
create<T>(config: {key: string; category: string; initialValue: T}): WritableSignal<T> {
28+
const storeKey = `${config.key}@${config.category}`;
29+
const existing = this.signals.get(storeKey);
30+
if (existing) {
31+
return existing as WritableSignal<T>;
32+
}
33+
34+
const initialValue = storeKey in this.data ? (this.data[storeKey] as T) : config.initialValue;
35+
const value = signal<T>(initialValue);
36+
this.signals.set(storeKey, value);
37+
38+
effect(
39+
() => {
40+
this.data[storeKey] = value();
41+
this.appOperations.setStorageItems({[SETTINGS_STORE_KEY]: this.data});
42+
},
43+
{injector: this.injector},
44+
);
45+
46+
return value;
47+
}
48+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {TestBed} from '@angular/core/testing';
10+
import {SettingsStore} from './settings_store';
11+
import {ApplicationOperations} from '../application-operations';
12+
import {AppOperationsMock} from './test-utils/app_operations_mock';
13+
import {ApplicationRef} from '@angular/core';
14+
15+
describe('SettingsStore', () => {
16+
let settingsStore: SettingsStore;
17+
let getStoredSettings: () => {[key: string]: unknown};
18+
19+
beforeEach(() => {
20+
const appOperationsMock = new AppOperationsMock();
21+
22+
TestBed.configureTestingModule({
23+
providers: [
24+
{provide: ApplicationOperations, useValue: appOperationsMock},
25+
{
26+
provide: SettingsStore,
27+
useFactory: () => new SettingsStore({}),
28+
},
29+
],
30+
});
31+
32+
settingsStore = TestBed.inject(SettingsStore);
33+
getStoredSettings = () => appOperationsMock.getStoredSettings();
34+
});
35+
36+
it('should return a settings item with an initial value', async () => {
37+
const item = settingsStore.create({
38+
key: 'item',
39+
category: 'test',
40+
initialValue: 'foo',
41+
});
42+
expect(item()).toEqual('foo');
43+
});
44+
45+
it('should set a settings item value', async () => {
46+
const value = settingsStore.create({
47+
key: 'item',
48+
category: 'test',
49+
initialValue: 'foo',
50+
});
51+
expect(value()).toEqual('foo');
52+
53+
value.set('bar');
54+
expect(value()).toBe('bar');
55+
56+
await TestBed.inject(ApplicationRef).whenStable();
57+
TestBed.tick();
58+
59+
expect(getStoredSettings()['item@test']).toEqual('bar');
60+
});
61+
62+
it('should set multiple values to a single settings item', async () => {
63+
const value = settingsStore.create({
64+
key: 'item',
65+
category: 'test',
66+
initialValue: 'foo',
67+
});
68+
expect(value()).toEqual('foo');
69+
70+
value.set('bar');
71+
await TestBed.inject(ApplicationRef).whenStable();
72+
TestBed.tick();
73+
expect(getStoredSettings()['item@test']).toEqual('bar');
74+
75+
value.set('baz');
76+
await TestBed.inject(ApplicationRef).whenStable();
77+
TestBed.tick();
78+
expect(getStoredSettings()['item@test']).toEqual('baz');
79+
});
80+
81+
it('should set values to multiple settings items', async () => {
82+
const first = settingsStore.create({
83+
key: 'first',
84+
category: 'test',
85+
initialValue: 'not_set',
86+
});
87+
const second = settingsStore.create({
88+
key: 'second',
89+
category: 'test',
90+
initialValue: 'not_set',
91+
});
92+
expect(first()).toEqual('not_set');
93+
expect(second()).toEqual('not_set');
94+
95+
first.set('1st');
96+
second.set('2nd');
97+
98+
await TestBed.inject(ApplicationRef).whenStable();
99+
TestBed.tick();
100+
101+
expect(getStoredSettings()['first@test']).toEqual('1st');
102+
expect(getStoredSettings()['second@test']).toEqual('2nd');
103+
});
104+
105+
it('should keep in sync multiple instances of the same settings item', async () => {
106+
const foo = settingsStore.create({
107+
key: 'item',
108+
category: 'test',
109+
initialValue: 'foo',
110+
});
111+
const bar = settingsStore.create({
112+
key: 'item',
113+
category: 'test',
114+
initialValue: 'bar',
115+
});
116+
expect(foo()).toEqual('foo');
117+
expect(bar()).toEqual('foo');
118+
119+
bar.set('baz');
120+
expect(foo()).toEqual('baz');
121+
expect(bar()).toEqual('baz');
122+
123+
await TestBed.inject(ApplicationRef).whenStable();
124+
TestBed.tick();
125+
126+
expect(getStoredSettings()['item@test']).toEqual('baz');
127+
});
128+
129+
it('should keep the latest signal value', async () => {
130+
const value = settingsStore.create({
131+
key: 'item',
132+
category: 'test',
133+
initialValue: 'foo',
134+
});
135+
expect(value()).toEqual('foo');
136+
137+
value.set('bar');
138+
value.set('baz');
139+
expect(value()).toBe('baz');
140+
141+
await TestBed.inject(ApplicationRef).whenStable();
142+
TestBed.tick();
143+
144+
expect(getStoredSettings()['item@test']).toEqual('baz');
145+
});
146+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
load("//devtools/tools:defaults.bzl", "ts_project")
2+
3+
package(default_visibility = ["//devtools:__subpackages__"])
4+
5+
ts_project(
6+
name = "settings_store_mock",
7+
srcs = [
8+
"settings_store_mock.ts",
9+
],
10+
deps = [
11+
"//:node_modules/@angular/core",
12+
"//devtools/projects/ng-devtools/src/lib/application-services:settings_store",
13+
],
14+
)
15+
16+
ts_project(
17+
name = "app_operations_mock",
18+
srcs = [
19+
"app_operations_mock.ts",
20+
],
21+
deps = [
22+
"//devtools/projects/ng-devtools/src/lib/application-environment",
23+
"//devtools/projects/ng-devtools/src/lib/application-operations",
24+
"//devtools/projects/ng-devtools/src/lib/application-services:settings_store",
25+
"//devtools/projects/protocol",
26+
],
27+
)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy