Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,11 @@ function updateRow(row: ToggleRow, values: string[]) {
)
)
panels.addIssue(props.issue.id)
for (const entry of row.entries) {
if (!panels.selected(entry.binding, entry.control) && next.has(optionValue(entry)))
panels.write(entry.binding, entry.control, true)
const added = row.entries.filter(
(entry) => !panels.selected(entry.binding, entry.control) && next.has(optionValue(entry)),
)
for (const entry of added) {
panels.write(entry.binding, entry.control, true)
}
for (const entry of row.entries) {
if (panels.selected(entry.binding, entry.control) && !next.has(optionValue(entry)))
Expand All @@ -210,9 +212,11 @@ function updateRow(row: ToggleRow, values: string[]) {
function updateOptions(values: string[]) {
if (pending.value || props.disabled) return
const next = new Set(values)
for (const entry of optionControls.value) {
if (!panels.selected(entry.binding, entry.control) && next.has(optionValue(entry)))
panels.write(entry.binding, entry.control, true)
const added = optionControls.value.filter(
(entry) => !panels.selected(entry.binding, entry.control) && next.has(optionValue(entry)),
)
for (const entry of added) {
panels.write(entry.binding, entry.control, true)
}
for (const entry of optionControls.value) {
if (panels.selected(entry.binding, entry.control) && !next.has(optionValue(entry)))
Expand All @@ -222,7 +226,10 @@ function updateOptions(values: string[]) {

function changeOptions(values: string[]) {
optionDraft.value = values
if (values.length) updateOptions(values)
if (values.length) {
updateOptions(values)
optionDraft.value = [...selectedOptionValues.value]
}
}

function openOptions() {
Expand Down
62 changes: 46 additions & 16 deletions apps/frontend/src/providers/project-review/review-panels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type ResolvedIssueControl = ResolvedIssueControlOptions &
| {
type: 'toggle'
id?: string
unselectToggles?: readonly string[]
issueCardDisplay?: {
icon?: string
label: string
Expand Down Expand Up @@ -329,11 +330,11 @@ export function createReviewPanels(
const bindings = new Map<string, ReviewPanelBinding>()
const projectV3 = project.value
if (!projectV3) return bindings
const issueIds = preview
? [...new Set([...selectedIssueIds.value, preview.control.issueId])]
: selectedIssueIds.value
const toggleIds = (issueId: string) => {
const ids = new Set(selectedToggleIds(projectV3.id, issueId))
if (preview?.control.type === 'toggle') {
for (const id of preview.control.unselectToggles ?? []) ids.delete(id)
}
if (preview?.control.type === 'toggle' && preview.control.issueId === issueId) {
for (const id of preview.control.childToggleIds ??
(preview.control.id === undefined ? [] : [preview.control.id])) {
Expand All @@ -342,6 +343,17 @@ export function createReviewPanels(
}
return [...ids]
}
const issueIds = preview
? [
...new Set([
...selectedIssueIds.value.filter(
(id) =>
session.read(projectV3.id, 'issue-active')[id] === true || toggleIds(id).length > 0,
),
preview.control.issueId,
]),
]
: selectedIssueIds.value
const showAllIssueControls = reviewData.value.showAllIssueControls === true
const context: ReviewContext = {
projectV3,
Expand Down Expand Up @@ -430,6 +442,7 @@ export function createReviewPanels(
...options,
type: 'toggle',
id: node.id,
unselectToggles: node.unselectToggles,
issueCardDisplay: node.issueCardDisplay
? {
icon: resolveWithContext(node.issueCardDisplay.icon, issueContext),
Expand Down Expand Up @@ -522,6 +535,14 @@ export function createReviewPanels(
control.childToggleIds = children.flatMap((item) =>
item.type === 'toggle' && !item.disabled && item.id ? [item.id] : [],
)
control.unselectToggles = [
...new Set([
...(control.unselectToggles ?? []),
...children.flatMap((item) =>
item.type === 'toggle' && !item.disabled ? (item.unselectToggles ?? []) : [],
),
]),
]
control.disabled ||= control.childToggleIds.length === 0
}
}
Expand Down Expand Up @@ -645,6 +666,16 @@ export function createReviewPanels(
return
}
if (typeof value !== 'boolean') return
if (value && current.unselectToggles?.length) {
const toggleIdsToUnselect = new Set(current.unselectToggles)
for (const [issueId, selected] of Object.entries(session.read(binding.projectId, 'issues'))) {
if (!(selected instanceof Set) || ![...selected].some((id) => toggleIdsToUnselect.has(id)))
continue
const keys = new Set([...selected].filter((id) => !toggleIdsToUnselect.has(id)))
session.write(binding.projectId, 'issues', issueId, keys.size ? keys : undefined)
if (issueId !== current.issueId) updateIssueOrder(binding.projectId, issueId)
}
}
if (current.childToggleIds) {
const keys = new Set(selectedToggleIds(binding.projectId, current.issueId))
for (const id of current.childToggleIds) {
Expand Down Expand Up @@ -839,22 +870,21 @@ export function createReviewPanels(
sharedInputs.textValues[control.key] = textValue(binding, control)
continue
}
const previewSelected =
control.type === 'toggle' &&
control.issueId === preview?.control.issueId &&
preview.control.type === 'toggle' &&
(control.id === preview.control.id ||
(control.id !== undefined && preview.control.childToggleIds?.includes(control.id)))
if (
control.childToggleIds ||
control.disabled ||
(control.type === 'toggle' &&
!(
binding.key === preview?.binding.key &&
control.issueId === preview.control.issueId &&
preview.control.type === 'toggle' &&
control.id === preview.control.id
) &&
!(
control.issueId === preview?.control.issueId &&
control.id !== undefined &&
preview.control.childToggleIds?.includes(control.id)
) &&
!selected(binding, control))
!previewSelected &&
(!selected(binding, control) ||
(preview?.control.type === 'toggle' &&
control.id !== undefined &&
preview.control.unselectToggles?.includes(control.id))))
)
continue
const selection: IssueSelection = issues.get(control.issueId) ?? {
Expand Down Expand Up @@ -896,7 +926,7 @@ export function createReviewPanels(
selected: {
items: {},
issueIds: preview
? [...new Set([...selectedIssueIds.value, preview.control.issueId])]
? [...issues].filter(([, { active }]) => active).map(([id]) => id)
: selectedIssueIds.value,
toggleIds: [...keys],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ export function expandItemReviewPanels(
: (ctx) => resolve(node.placeholder, scopedContext(ctx, item)) ?? ''
}
if (node.type !== 'toggle') return [scoped]
if (scoped.type === 'toggle') {
scoped.unselectToggles = node.unselectToggles?.map((id) => `${item.key}:${id}`)
}
if (scoped.type === 'toggle' && node.issueCardDisplay !== undefined) {
const display = node.issueCardDisplay
scoped.issueCardDisplay = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export interface IssueToggle extends IssueControlOptions {
* Without an ID, the toggle activates the issue directly and contributes no selected toggle ID.
*/
id?: string
/** Explicit toggle IDs to clear across issues when enabled. Item panels scope these IDs to the current item. */
unselectToggles?: readonly string[]
}

export type IssueToggleConfig = Omit<IssueToggle, 'type'>
Expand Down
21 changes: 12 additions & 9 deletions packages/moderation/src/data/issues/disclosures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,9 +468,10 @@ export const telemetryDisclosureReviewPanel = panel({
label: 'Telemetry: Opt In',
group: 'Missing',
},
disabled: ({ selected }) =>
selected.toggleIds.includes('telemetry-disclosure-missing-opt-out') ||
selected.toggleIds.includes('telemetry-disclosure-missing-always'),
unselectToggles: [
'telemetry-disclosure-missing-opt-out',
'telemetry-disclosure-missing-always',
],
}),
toggle({
issue: disclosuresIssue,
Expand All @@ -480,9 +481,10 @@ export const telemetryDisclosureReviewPanel = panel({
label: 'Telemetry: Opt Out',
group: 'Missing',
},
disabled: ({ selected }) =>
selected.toggleIds.includes('telemetry-disclosure-missing-opt-in') ||
selected.toggleIds.includes('telemetry-disclosure-missing-always'),
unselectToggles: [
'telemetry-disclosure-missing-opt-in',
'telemetry-disclosure-missing-always',
],
}),
toggle({
issue: disclosuresIssue,
Expand All @@ -492,9 +494,10 @@ export const telemetryDisclosureReviewPanel = panel({
label: 'Telemetry: Always Online',
group: 'Missing',
},
disabled: ({ selected }) =>
selected.toggleIds.includes('telemetry-disclosure-missing-opt-in') ||
selected.toggleIds.includes('telemetry-disclosure-missing-opt-out'),
unselectToggles: [
'telemetry-disclosure-missing-opt-in',
'telemetry-disclosure-missing-opt-out',
],
}),
),
)
Expand Down
4 changes: 2 additions & 2 deletions packages/moderation/src/data/issues/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,13 @@ export const rulesReviewPanel = panel({
issue: rulesServerSideOptOutIssue,
label: 'Opt-out',
id: 'rules-server-side-opt-out',
disabled: ({ selected }) => selected.toggleIds.includes('rules-server-side-opt-in'),
unselectToggles: ['rules-server-side-opt-in'],
}),
toggle({
issue: rulesServerSideOptInIssue,
label: 'Opt-in',
id: 'rules-server-side-opt-in',
disabled: ({ selected }) => selected.toggleIds.includes('rules-server-side-opt-out'),
unselectToggles: ['rules-server-side-opt-out'],
}),
toggle({
issue: rulesAiGeneratedIssue,
Expand Down
Loading