Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wendy+jacob+nick/eng 2908 users can manage approval requirements #5351

Merged
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
10 changes: 10 additions & 0 deletions app/web/src/api/sdf/dal/property_editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export interface PropertyEditorPropWidgetKindArray {
kind: "array";
}

export interface PropertyEditorpropWidgetKindRequirement {
kind: "requirement";
}

export interface PropertyEditorPropWidgetKindUsers {
kind: "users";
}

export interface PropertyEditorPropWidgetKindCheckBox {
kind: "checkbox";
}
Expand Down Expand Up @@ -85,6 +93,8 @@ export type PropertyEditorPropWidgetKind =
| PropertyEditorPropWidgetKindInteger
| PropertyEditorPropWidgetKindHeader
| PropertyEditorPropWidgetKindArray
| PropertyEditorpropWidgetKindRequirement
| PropertyEditorPropWidgetKindUsers
| PropertyEditorPropWidgetKindCodeEditor
| PropertyEditorPropWidgetKindComboBox
| PropertyEditorPropWidgetKindSelect
Expand Down
12 changes: 12 additions & 0 deletions app/web/src/api/sdf/dal/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import {
DiagramViewData,
SocketLocationInfo,
} from "@/components/ModelingDiagram/diagram_types";
import { UserId } from "@/store/auth.store";
import { ComponentType } from "./schema";

export type ViewId = string;
export type EntityId = string; // TODO - "entity" can refer to most things in the system, currently we use this mostly just for views

export type Components = Record<ComponentId, IRect>;
export type Groups = Record<
Expand Down Expand Up @@ -39,3 +41,13 @@ export interface StringGeometry {
width: string;
height: string;
}

// Approval Requirement Definition types
export type ApprovalRequirementDefinitionId = string;
export interface ViewApprovalRequirementDefinition {
id: ApprovalRequirementDefinitionId;
entityId: ViewId;
requiredCount: number;
approverGroups: Record<string, UserId[]>;
approverIndividuals: UserId[];
}
2 changes: 1 addition & 1 deletion app/web/src/components/Actions/ActionsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</div>
</div>
<div class="flex-none font-bold">
{{ displayActions.length }} Actions
{{ displayActions.length }} Action(s)
</div>
<!-- TODO(Wendy) - maybe a PillCounter makes more sense here? -->
<!-- <PillCounter
Expand Down
4 changes: 2 additions & 2 deletions app/web/src/components/ApprovalFlowModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import ActionsList from "./Actions/ActionsList.vue";

const changeSetsStore = useChangeSetsStore();
const authStore = useAuthStore();
const ffStore = useFeatureFlagsStore();
const featureFlagsStore = useFeatureFlagsStore();
const toast = useToast();

const modalRef = ref<InstanceType<typeof Modal> | null>(null);
Expand All @@ -65,7 +65,7 @@ async function openModalHandler() {
if (changeSet?.value?.name === "HEAD") return;

userIsApprover.value = changeSetsStore.currentUserIsDefaultApprover;
if (ffStore.WORKSPACE_FINE_GRAINED_ACCESS_CONTROL)
if (featureFlagsStore.WORKSPACE_FINE_GRAINED_ACCESS_CONTROL)
userIsApprover.value = false;

modalRef.value?.open();
Expand Down
139 changes: 122 additions & 17 deletions app/web/src/components/AttributesPanel/TreeFormItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,28 @@
)
"
>
<template v-if="treeDef.children.length === 0">(empty)</template>
<template v-else-if="treeDef.children.length === 1"
>(1 item)</template
>
<template v-else>({{ treeDef.children.length }} items)</template>
<template v-if="widgetKind === 'users'">
<template v-if="treeDef.children.length === 0"
>(empty)</template
>
<template v-else-if="treeDef.children.length === 1"
>(1 user)</template
>
<template v-else
>({{ treeDef.children.length }} users)</template
>
wendybujalski marked this conversation as resolved.
Show resolved Hide resolved
</template>
<template v-else>
<template v-if="treeDef.children.length === 0"
>(empty)</template
>
<template v-else-if="treeDef.children.length === 1"
>(1 item)</template
>
<template v-else
>({{ treeDef.children.length }} items)</template
>
</template>
</div>
</div>
<SourceIconWithTooltip
Expand Down Expand Up @@ -205,6 +222,21 @@
v-show="isOpen && headerHasContent"
class="attributes-panel-item__children"
>
<div
v-if="widgetKind === 'users' && isArray && propManual"
:style="{ marginLeft: indentPx }"
class="flex flex-col grow gap-xs relative pt-2xs px-xs"
>
<div class="text-xs">Add an approver user for this requirement -</div>
<UserSelectMenu
ref="userSelectMenuRef"
class="flex-none"
noUsersLabel="All users are approvers!"
:usersToFilterOut="usersToFilterOut"
@select="addUser"
/>
</div>

<TreeFormItem
v-for="childProp in treeDef.children"
:key="`${propName}/${childProp.propDef?.name}`"
Expand All @@ -229,8 +261,27 @@
</div>
</div>

<template v-if="(isArray || isMap) && propManual">
<template
v-if="
(isArray || isMap || widgetKind === 'requirement') && propManual
"
>
nickgerace marked this conversation as resolved.
Show resolved Hide resolved
<div
v-if="widgetKind === 'requirement'"
:style="{ marginLeft: indentPx }"
class="flex flex-row grow relative overflow-hidden items-center justify-center pt-xs"
>
<VButton
label="Delete Requirement"
tone="destructive"
variant="ghost"
icon="trash"
size="sm"
@click="deleteRequirement"
/>
</div>
<div
v-else-if="widgetKind !== 'users'"
:style="{ marginLeft: indentPx }"
class="h-[34px] flex flex-row grow gap-xs relative overflow-hidden items-center pt-2xs"
>
Expand Down Expand Up @@ -455,6 +506,7 @@
</div>
<!-- Actual input, to the right -->
<div
v-if="widgetKind !== 'users'"
:class="
clsx(
'attributes-panel-item__input-wrap group/input',
Expand Down Expand Up @@ -726,6 +778,21 @@
@click="openNonEditableModal"
/>
</div>
<!-- users widget is just a delete button -->
<IconButton
v-else
icon="trash"
iconTone="destructive"
iconIdleTone="shade"
:tooltip="
treeDef.propDef.isReadonly
? 'Can\'t Remove Only Approver!'
: 'Remove Approver'
"
size="xs"
:disabled="treeDef.propDef.isReadonly"
@click="() => removeUser(treeDef.propId)"
/>
</div>

<!-- VALIDATION DETAILS -->
Expand Down Expand Up @@ -938,6 +1005,7 @@ import SecretsModal from "../SecretsModal.vue";
import SourceIconWithTooltip from "./SourceIconWithTooltip.vue";
import CodeViewer from "../CodeViewer.vue";
import { TreeFormContext } from "./TreeForm.vue";
import UserSelectMenu from "../UserSelectMenu.vue";

export type TreeFormProp = {
id: string;
Expand Down Expand Up @@ -1052,7 +1120,13 @@ const viewsStore = useViewsStore();
const componentId = viewsStore.selectedComponentId!;

const changeSetsStore = useChangeSetsStore();
const attributesStore = useComponentAttributesStore(componentId);
const attributesStore = computed(() => {
wendybujalski marked this conversation as resolved.
Show resolved Hide resolved
if (props.attributesPanel) {
return useComponentAttributesStore(componentId);
} else {
return undefined;
}
});
const secretsStore = useSecretsStore();

const fullPropDef = computed(() => props.treeDef.propDef);
Expand Down Expand Up @@ -1239,10 +1313,12 @@ const propSource = computed<AttributeValueSource>(() => {
});

const setSource = (source: AttributeValueSource) => {
if (!attributesStore.value) return;

if (source === AttributeValueSource.Manual) {
const value = props.treeDef.value?.value ?? null;

attributesStore.UPDATE_PROPERTY_VALUE({
attributesStore.value.UPDATE_PROPERTY_VALUE({
update: {
attributeValueId: props.treeDef.valueId,
parentAttributeValueId: props.treeDef.parentValueId,
Expand All @@ -1253,7 +1329,7 @@ const setSource = (source: AttributeValueSource) => {
},
});
} else {
attributesStore.RESET_PROPERTY_VALUE({
attributesStore.value.RESET_PROPERTY_VALUE({
attributeValueId: props.treeDef.valueId,
});
}
Expand Down Expand Up @@ -1334,8 +1410,8 @@ const newMapChildKeyIsValid = computed(() => {

function removeChildHandler() {
if (!isChildOfArray.value && !isChildOfMap.value) return;
if (props.attributesPanel) {
attributesStore.REMOVE_PROPERTY_VALUE({
if (props.attributesPanel && attributesStore.value) {
attributesStore.value.REMOVE_PROPERTY_VALUE({
attributeValueId: props.treeDef.valueId,
propId: props.treeDef.propId,
componentId,
Expand Down Expand Up @@ -1371,8 +1447,8 @@ function addChildHandler() {
return;
}

if (props.attributesPanel) {
attributesStore.UPDATE_PROPERTY_VALUE({
if (props.attributesPanel && attributesStore.value) {
attributesStore.value.UPDATE_PROPERTY_VALUE({
insert: {
parentAttributeValueId: props.treeDef.valueId,
propId: props.treeDef.propId,
Expand All @@ -1391,8 +1467,8 @@ function unsetHandler(value?: string) {
newValueBoolean.value = false;
newValueString.value = "";

if (props.attributesPanel) {
attributesStore.RESET_PROPERTY_VALUE({
if (props.attributesPanel && attributesStore.value) {
attributesStore.value.RESET_PROPERTY_VALUE({
attributeValueId: props.treeDef.valueId,
});
} else {
Expand Down Expand Up @@ -1445,8 +1521,8 @@ function updateValue(maybeNewVal?: unknown) {
isForSecret = true;
}

if (props.attributesPanel) {
attributesStore.UPDATE_PROPERTY_VALUE({
if (props.attributesPanel && attributesStore.value) {
attributesStore.value.UPDATE_PROPERTY_VALUE({
update: {
attributeValueId: props.treeDef.valueId,
parentAttributeValueId: props.treeDef.parentValueId,
Expand Down Expand Up @@ -1703,6 +1779,35 @@ const socketSearchFilters = computed(() => {

return filters;
});

// APPROVAL REQUIREMENTS STUFF
const usersToFilterOut = computed(() => {
if (props.treeDef.propDef.widgetKind.kind === "users") {
const users = props.treeDef.children.map((user) => user.propId);
return users;
} else return undefined;
});

const userSelectMenuRef = ref<InstanceType<typeof UserSelectMenu>>();

const addUser = async (userId: string) => {
const requirementId = props.treeDef.parentValueId;
await viewsStore.ADD_INDIVIDUAL_APPROVER_TO_REQUIREMENT(
requirementId,
userId,
);
userSelectMenuRef.value?.clearSelection();
};

const removeUser = (userId: string) => {
const requirementId = props.treeDef.parentValueId;
viewsStore.REMOVE_INDIVIDUAL_APPROVER_FROM_REQUIREMENT(requirementId, userId);
};

const deleteRequirement = () => {
const requirementId = props.treeDef.propId;
viewsStore.REMOVE_VIEW_APPROVAL_REQUIREMENT(requirementId);
};
</script>

<style lang="less">
Expand Down
16 changes: 8 additions & 8 deletions app/web/src/components/ChangesPanelProposed.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div>
<div class="h-full flex flex-col overflow-hidden">
<ConfirmHoldModal ref="confirmRef" :ok="finishHold" />
<div v-if="actionsStore.proposedActions.length > 0">
<!-- TODO(Wendy)- SEARCH BAR SHOULD GO HERE -->
Expand Down Expand Up @@ -34,7 +34,7 @@
:open="!!singleSelectedAction"
:selectedTab="selectedTab"
/>
<div v-if="changeSetStore.headSelected">
<template v-if="changeSetStore.headSelected">
<ActionsList
v-if="actionsStore.proposedActions.length > 0"
:clickAction="clickAction"
Expand All @@ -47,8 +47,8 @@
primaryText="All Actions on HEAD have been run"
secondaryText="You can see those actions in the history tab."
/>
</div>
<div v-else>
</template>
<template v-else>
<TreeNode
enableDefaultHoverClasses
enableGroupToggle
Expand All @@ -57,7 +57,7 @@
leftBorderSize="none"
defaultOpen
internalScrolling
class="min-h-[50vh]"
class="min-h-[32px]"
primaryIconClasses=""
label="Proposed Actions In This Change Set"
>
Expand Down Expand Up @@ -99,11 +99,11 @@
<EmptyStateCard
v-else
iconName="actions"
primaryText="All Actions on HEAD have been run"
secondaryText="You can see those actions in the history tab."
primaryText="All Actions on HEAD have run"
wendybujalski marked this conversation as resolved.
Show resolved Hide resolved
secondaryText="See past actions in the history tab."
/>
</TreeNode>
</div>
</template>
</div>
</template>

Expand Down
2 changes: 1 addition & 1 deletion app/web/src/components/ComponentCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
:class="
clsx(
'p-xs border-l-4 border relative',
titleCard ? 'mb-xs' : 'rounded-md',
!titleCard && 'rounded-md',
'toDelete' in component.def && component.def.toDelete && 'opacity-70',
'fromBaseChangeSet' in component.def &&
component.def.fromBaseChangeSet &&
Expand Down
2 changes: 1 addition & 1 deletion app/web/src/components/ComponentDetails.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<ScrollArea>
<template #top>
<ComponentCard :component="props.component" titleCard>
<ComponentCard :component="props.component" titleCard class="mb-xs">
<DetailsPanelMenuIcon
:selected="props.menuSelected"
@click="
Expand Down
Loading