Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/**
 * Buttons for the bottom bar. Defined separately so the logic code is more readable.
 * Also they can be reused in custom buttons.
 * `on*OnComment()` are used for actions with comment attached to them.
 */
 
import { inject, observer } from "mobx-react";
import type React from "react";
import { memo, type ReactElement } from "react";
import { Tooltip, Button } from "@humansignal/ui";
import { IconInfoOutline } from "@humansignal/icons";
import type { MSTStore } from "../../stores/types";
 
type MixedInParams = {
  store: MSTStore;
  history: any;
};
 
export function controlsInjector<T extends {}>(fn: (props: T & MixedInParams) => ReactElement) {
  const wrapped = inject(({ store }) => {
    return {
      store,
      history: store?.annotationStore?.selected?.history,
    };
  })(fn);
  // inject type doesn't handle the injected props, so we have to force cast it
  return wrapped as unknown as (props: T) => ReactElement;
}
 
type ButtonTooltipProps = {
  title: string;
  children: JSX.Element;
};
 
export const ButtonTooltip = controlsInjector<ButtonTooltipProps>(
  observer(({ store, title, children }) => {
    return (
      <Tooltip title={title} disabled={!store.settings.enableTooltips}>
        {children}
      </Tooltip>
    );
  }),
);
 
type AcceptButtonProps = {
  disabled: boolean;
  history: any;
  store: MSTStore;
};
 
export const AcceptButton = memo(
  observer(({ disabled, history, store }: AcceptButtonProps) => {
    const annotation = store.annotationStore.selected;
    // changes in current sessions or saved draft
    const hasChanges = history.canUndo || annotation.versions.draft;
 
    return (
      <Button
        key="accept"
        tooltip="Accept annotation: [ Ctrl+Enter ]"
        aria-label="accept-annotation"
        disabled={disabled}
        onClick={async () => {
          annotation.submissionInProgress();
          await store.commentStore.commentFormSubmit();
          store.acceptAnnotation();
        }}
      >
        {hasChanges ? "Fix + Accept" : "Accept"}
      </Button>
    );
  }),
);
 
export const RejectButtonDefinition = {
  id: "reject",
  name: "reject",
  title: "Reject",
  variant: "negative",
  look: "outlined",
  ariaLabel: "reject-annotation",
  tooltip: "Reject annotation: [ Ctrl+Space ]",
  // @todo we need this for types compatibility, but better to fix CustomButtonType
  disabled: false,
};
 
type SkipButtonProps = {
  disabled: boolean;
  store: MSTStore;
  /**
   * Handler wrapper for skip with required comment,
   * conditions are checked in wrapper and if all good the `action` is called.
   **/
  onSkipWithComment: (event: React.MouseEvent, action: () => any) => void;
};
 
// Manager roles that can force-skip unskippable tasks (OW=Owner, AD=Admin, MA=Manager)
const MANAGER_ROLES = ["OW", "AD", "MA"];
 
export const SkipButton = memo(
  observer(({ disabled, store, onSkipWithComment }: SkipButtonProps) => {
    const task = store.task;
    const taskAllowSkip = (task as any)?.allow_skip !== false;
    const userRole = (window as any).APP_SETTINGS?.user?.role;
    const hasForceSkipPermission = MANAGER_ROLES.includes(userRole);
    const canSkip = taskAllowSkip || hasForceSkipPermission;
    const isDisabled = disabled || !canSkip;
 
    const tooltip: string = canSkip ? "Cancel (skip) task [ Ctrl+Space ]" : "This task cannot be skipped";
 
    const showInfoIcon = !taskAllowSkip && hasForceSkipPermission;
 
    return (
      <>
        {showInfoIcon && (
          <Tooltip title="Annotators and Reviewers will not be able to skip this task">
            <IconInfoOutline width={20} height={20} className="text-neutral-content ml-auto cursor-pointer" />
          </Tooltip>
        )}
        <Button
          key="skip"
          aria-label="skip-task"
          disabled={isDisabled}
          look="outlined"
          tooltip={tooltip}
          onClick={async (e) => {
            if (!canSkip) return;
            const action = () => store.skipTask({});
            const selected = store.annotationStore?.selected;
            if (store.hasInterface("comments:skip") ?? true) {
              onSkipWithComment(e, action);
            } else {
              selected?.submissionInProgress();
              await store.commentStore.commentFormSubmit();
              store.skipTask({});
            }
          }}
        >
          Skip
        </Button>
      </>
    );
  }),
);
 
export const UnskipButton = memo(
  observer(({ disabled, store }: { disabled: boolean; store: MSTStore }) => {
    return (
      <Button
        key="cancel-skip"
        tooltip="Cancel skip: []"
        aria-label="cancel-skip"
        look="outlined"
        disabled={disabled}
        onClick={async () => {
          const selected = store.annotationStore?.selected;
 
          selected?.submissionInProgress();
          await store.commentStore.commentFormSubmit();
          store.unskipTask();
        }}
      >
        Cancel skip
      </Button>
    );
  }),
);