diff --git a/server/app.ts b/server/app.ts index f5b1a924f..0a50c5ec1 100644 --- a/server/app.ts +++ b/server/app.ts @@ -78,6 +78,12 @@ class Events extends EventEmitter { dmxConfigs: ClassesImport.DMXConfig[] = []; dmxSets: ClassesImport.DMXSet[] = []; hackingPresets: ClassesImport.HackingPreset[] = []; + printQueue: { + id: string; + asset: string; + simulatorId: string; + timestamp: number; + }[] = []; autoUpdate = true; migrations: any = {assets: true, dmx: false}; thoriumId: string = randomWords(5).join("-"); diff --git a/server/events/eventMacro.js b/server/events/eventMacro.js index c41ec90f6..1d3f1e582 100644 --- a/server/events/eventMacro.js +++ b/server/events/eventMacro.js @@ -109,7 +109,10 @@ App.on("triggerMacroButton", ({simulatorId, configId, buttonId}) => { const macro = App.macroButtonConfigs.find(s => s.id === configId); const button = macro.getButton(buttonId); // Don't allow a macro to trigger itself - const macros = button.actions; + const excludedActions = ["printPdf"]; + const macros = button.actions.filter( + ({event}) => !excludedActions.includes(event), + ); if (macros.length > 0) App.handleEvent({simulatorId, macros}, "triggerMacros"); }); diff --git a/server/events/ship.js b/server/events/ship.js index dcfeee45c..3b6c24883 100644 --- a/server/events/ship.js +++ b/server/events/ship.js @@ -188,3 +188,42 @@ App.on( }); }, ); +App.on("printPdf", ({asset, simulatorId}) => { + App.printQueue.push({ + id: uuid.v4(), + asset, + simulatorId, + timestamp: Date.now(), + }); + pubsub.publish( + "printQueue", + App.printQueue.filter(queue => queue.simulatorId === simulatorId), + ); + App.handleEvent( + { + simulatorId: simulatorId, + component: "PrintQueueCore", + title: `New Print Queue`, + body: asset, + color: "info", + }, + "addCoreFeed", + ); + pubsub.publish("notify", { + id: uuid.v4(), + simulatorId: simulatorId, + type: "Print Queue", + station: "Core", + title: `New Print Queue`, + body: asset, + color: "info", + }); +}); +App.on("clearPdf", ({id}) => { + const pdf = App.printQueue.find(item => item.id === id); + App.printQueue = App.printQueue.filter(item => item.id !== id); + pubsub.publish( + "printQueue", + App.printQueue.filter(queue => queue.simulatorId === pdf.simulatorId), + ); +}); diff --git a/server/helpers/defaultSnapshot.js b/server/helpers/defaultSnapshot.js index b24e1bd21..22124f3ed 100644 --- a/server/helpers/defaultSnapshot.js +++ b/server/helpers/defaultSnapshot.js @@ -13637,6 +13637,7 @@ export default { dmxConfigs: [], dmxDevices: [], hackingPresets: [], + printQueue: [], autoUpdate: true, thoriumId: randomWords(5).join("-"), doTrack: false, diff --git a/server/typeDefs/ship.ts b/server/typeDefs/ship.ts index e4fc5902f..748cb7a20 100644 --- a/server/typeDefs/ship.ts +++ b/server/typeDefs/ship.ts @@ -1,5 +1,8 @@ import {gql, withFilter} from "apollo-server-express"; +import App from "../app"; import {pubsub} from "../helpers/subscriptionManager"; +import uuid from "uuid"; + const mutationHelper = require("../helpers/mutationHelper").default; // We define a schema that encompasses all of the types // necessary for the functionality in this file. @@ -100,11 +103,20 @@ const schema = gql` Macro: Actions: Print PDF Asset """ printPdf(asset: String!): String + clearPdf(id: ID!): String + } + + type PrintQueue { + id: ID! + simulatorId: String! + asset: String! + timestamp: Float! } extend type Subscription { notify(simulatorId: ID!, station: String, trigger: String): Notification widgetNotify(simulatorId: ID!, station: String): String + printQueue(simulatorId: ID!): [PrintQueue] } `; @@ -148,6 +160,26 @@ const resolver = { rootValue => !!rootValue, ), }, + printQueue: { + resolve(rootValue) { + return rootValue; + }, + subscribe: withFilter( + (rootValue, {simulatorId}) => { + const id = uuid.v4(); + process.nextTick(() => { + let returnVal = App.printQueue.filter( + queue => queue.simulatorId === simulatorId, + ); + pubsub.publish(id, returnVal); + }); + return pubsub.asyncIterator([id, "printQueue"]); + }, + (rootValue, {simulatorId}) => { + return true; + }, + ), + }, }, }; diff --git a/src/components/client/lighting.tsx b/src/components/client/lighting.tsx index 9643cd63f..1944b45f3 100644 --- a/src/components/client/lighting.tsx +++ b/src/components/client/lighting.tsx @@ -21,7 +21,9 @@ export const ClientLighting: React.FC<{ const {data} = useDmxSetsSubscription(); React.useEffect(() => { - activate({variables: {clientId, dmxSetId}}); + if (dmxSetId) { + activate({variables: {clientId, dmxSetId}}); + } }, [activate, clientId, dmxSetId]); if (!data?.dmxSets || data?.dmxSets.length === 0) { diff --git a/src/components/core/menubar/notificationConfig.js b/src/components/core/menubar/notificationConfig.js index 1f8ef1017..9bbfa9952 100644 --- a/src/components/core/menubar/notificationConfig.js +++ b/src/components/core/menubar/notificationConfig.js @@ -38,6 +38,7 @@ export const notifyComponents = [ "Medical Teams", "Navigation", "Phasers", + "Print Queue", "Probes", "Railgun", "Reactivation Code", diff --git a/src/components/views/Macros/index.js b/src/components/views/Macros/index.js index accde6e0c..52238fc88 100644 --- a/src/components/views/Macros/index.js +++ b/src/components/views/Macros/index.js @@ -7,7 +7,7 @@ import EventPicker from "containers/FlightDirector/MissionConfig/EventPicker"; import uuid from "uuid"; import MacroConfig from "./macroConfig"; import {FaBan} from "react-icons/fa"; -import triggerLocalMacros from "helpers/triggerLocalMacros"; +import triggerLocalMacros, {localMacrosList} from "helpers/triggerLocalMacros"; class MacrosCore extends Component { state = {actions: []}; render() { @@ -134,7 +134,9 @@ class MacrosCore extends Component { `} variables={{ simulatorId: simulator.id, - macros: actions.map(({id, ...rest}) => rest), + macros: actions + .filter(({event}) => localMacrosList.includes(event)) + .map(({id, ...rest}) => rest), }} > {triggerMacros => ( diff --git a/src/components/views/Macros/macroButtons.js b/src/components/views/Macros/macroButtons.js index 75e13511e..8b695d27e 100644 --- a/src/components/views/Macros/macroButtons.js +++ b/src/components/views/Macros/macroButtons.js @@ -5,6 +5,7 @@ import {useQuery, useMutation} from "@apollo/client"; import {Input, Button} from "helpers/reactstrap"; import useLocalStorage from "helpers/hooks/useLocalStorage"; import {capitalCase} from "change-case"; +import triggerLocalMacros from "helpers/triggerLocalMacros"; const fragment = gql` fragment MacrosButtonsData on MacroButtonConfig { @@ -15,6 +16,12 @@ const fragment = gql` name color category + actions { + id + event + args + delay + } } } `; @@ -108,15 +115,16 @@ const MacroButtons = ({simulator: {id: simulatorId}}) => { key={b.id} color={b.color} size="sm" - onClick={() => + onClick={() => { + triggerLocalMacros(b.actions); trigger({ variables: { configId: selectedConfig, buttonId: b.id, simulatorId, }, - }) - } + }); + }} > {b.name} diff --git a/src/components/views/PrintQueue/clear.graphql b/src/components/views/PrintQueue/clear.graphql new file mode 100644 index 000000000..a7f0e4663 --- /dev/null +++ b/src/components/views/PrintQueue/clear.graphql @@ -0,0 +1,3 @@ +mutation ClearPdf($id: ID!) { + clearPdf(id: $id) +} diff --git a/src/components/views/PrintQueue/core.tsx b/src/components/views/PrintQueue/core.tsx new file mode 100644 index 000000000..4b0005fc6 --- /dev/null +++ b/src/components/views/PrintQueue/core.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { + Simulator, + usePrintQueueSubscription, + useClearPdfMutation, +} from "generated/graphql"; +import "./style.scss"; +import {TimeCounter} from "../Sensors"; +import printJS from "print-js"; + +interface TemplateProps { + children: React.ReactNode; + simulator: Simulator; +} + +const PrintQueue: React.FC = props => { + const {simulator} = props; + const {loading, data} = usePrintQueueSubscription({ + variables: {simulatorId: simulator.id}, + }); + + const [clearPdf] = useClearPdfMutation(); + + if (loading || !data) return null; + const {printQueue} = data; + if (!printQueue || printQueue?.length === 0) + return
No Items Queued
; + return ( + + ); +}; +export default PrintQueue; diff --git a/src/components/views/PrintQueue/query.graphql b/src/components/views/PrintQueue/query.graphql new file mode 100644 index 000000000..095c05efb --- /dev/null +++ b/src/components/views/PrintQueue/query.graphql @@ -0,0 +1,7 @@ +subscription PrintQueue($simulatorId: ID!) { + printQueue(simulatorId: $simulatorId) { + id + asset + timestamp + } +} diff --git a/src/components/views/PrintQueue/style.scss b/src/components/views/PrintQueue/style.scss new file mode 100644 index 000000000..acef3db22 --- /dev/null +++ b/src/components/views/PrintQueue/style.scss @@ -0,0 +1,7 @@ +.core-printqueue { + li { + display: flex; + gap: 0.5rem; + margin-top: 0.25rem; + } +} diff --git a/src/components/views/Sensors/index.tsx b/src/components/views/Sensors/index.tsx index 0929a0536..199d6c032 100644 --- a/src/components/views/Sensors/index.tsx +++ b/src/components/views/Sensors/index.tsx @@ -125,7 +125,7 @@ function calculateTime(milliseconds: number) { ); } -const TimeCounter: React.FC<{time: Date}> = ({time}) => { +export const TimeCounter: React.FC<{time: Date}> = ({time}) => { const milliseconds = Date.now() - Number(time); return <>{calculateTime(milliseconds)}; }; diff --git a/src/components/views/Timeline/classicMission.tsx b/src/components/views/Timeline/classicMission.tsx index 289a02810..3ed079f50 100644 --- a/src/components/views/Timeline/classicMission.tsx +++ b/src/components/views/Timeline/classicMission.tsx @@ -50,26 +50,27 @@ const TimelineStep: React.FC = ({ const runMacro = (t: TimelineStepI) => { if (!t) return; const stepIndex = timeline.findIndex(i => i.id === t.id); - const macros = t.timelineItems - .filter(a => - onlyExecuteViewscreen ? allowedMacros.indexOf(a.event) > -1 : true, - ) - .map(tt => { - const args = !tt.args - ? "{}" - : typeof tt.args === "string" - ? JSON.stringify({...JSON.parse(tt.args)}) - : JSON.stringify({...(tt.args as Object)}); - return { - stepId: tt.id, - event: tt.event, - args, - delay: tt.delay || 0, - }; - }); + const macros = t.timelineItems.map(tt => { + const args = !tt.args + ? "{}" + : typeof tt.args === "string" + ? JSON.stringify({...JSON.parse(tt.args)}) + : JSON.stringify({...(tt.args as Object)}); + return { + stepId: tt.id, + event: tt.event, + args, + delay: tt.delay || 0, + }; + }); + const filteredMacros = macros.filter( + a => + (onlyExecuteViewscreen ? allowedMacros.indexOf(a.event) > -1 : true) && + !excludedTimelineActions.includes(a.event), + ); const variables = { simulatorId, - macros, + macros: filteredMacros, }; triggerLocalMacros(macros); triggerMacros({variables}); diff --git a/src/components/views/Timeline/timelineControl.tsx b/src/components/views/Timeline/timelineControl.tsx index c50a73131..a93b426e5 100644 --- a/src/components/views/Timeline/timelineControl.tsx +++ b/src/components/views/Timeline/timelineControl.tsx @@ -2,7 +2,7 @@ import React, {Fragment} from "react"; import {Button, ButtonGroup} from "helpers/reactstrap"; import StepModal from "./stepModal"; import {FaArrowLeft, FaStepForward, FaPlay, FaArrowRight} from "react-icons/fa"; -import triggerLocalMacros from "helpers/triggerLocalMacros"; +import triggerLocalMacros, {localMacrosList} from "helpers/triggerLocalMacros"; import { TimelineStep, useSetSimulatorTimelineStepMutation, @@ -25,7 +25,9 @@ interface TimelineControl { auxTimelineId?: string; } -export const excludedTimelineActions = ["setSimulatorMission"]; +export const excludedTimelineActions = ["setSimulatorMission"].concat( + localMacrosList, +); const TimelineControl: React.FC = ({ actions, @@ -44,26 +46,29 @@ const TimelineControl: React.FC = ({ const currentStep = timeline[currentTimelineStep]; if (!currentStep) return; - const macros = currentStep.timelineItems - .filter(t => actions[t.id] && !excludedTimelineActions.includes(t.event)) - .map(t => { - const args = !t.args - ? "{}" - : typeof t.args === "string" - ? JSON.stringify({...JSON.parse(t.args)}) - : JSON.stringify({...(t.args as Object)}); - const stepDelay = delay[t.id]; + const macros = currentStep.timelineItems.map(t => { + const args = !t.args + ? "{}" + : typeof t.args === "string" + ? JSON.stringify({...JSON.parse(t.args)}) + : JSON.stringify({...(t.args as Object)}); + const stepDelay = delay[t.id]; + + return { + stepId: t.id, + event: t.event, + args, + delay: stepDelay || stepDelay === 0 ? stepDelay : t.delay || 0, + }; + }); + + const filteredMacros = macros.filter( + t => actions[t.stepId] && !excludedTimelineActions.includes(t.event), + ); - return { - stepId: t.id, - event: t.event, - args, - delay: stepDelay || stepDelay === 0 ? stepDelay : t.delay || 0, - }; - }); const variables = { simulatorId, - macros, + macros: filteredMacros, }; triggerLocalMacros(macros); triggerMacros({variables}); diff --git a/src/components/views/index.ts b/src/components/views/index.ts index cb924a9d5..142f06b2c 100644 --- a/src/components/views/index.ts +++ b/src/components/views/index.ts @@ -222,6 +222,7 @@ const CountermeasuresCore = React.lazy(() => import("./Countermeasures/core")); const TaskFlowCore = React.lazy(() => import("./Tasks/taskFlowCore")); const DocumentsCore = React.lazy(() => import("./Documents/core")); const HackingCore = React.lazy(() => import("./Hacking/core")); +const PrintQueueCore = React.lazy(() => import("./PrintQueue/core")); // Widgets const ComposerWidget = React.lazy(() => import("./LongRangeComm/Composer")); @@ -528,6 +529,7 @@ export const Cores = { TaskFlowCore, DocumentsCore, HackingCore, + PrintQueueCore, }; export default Views; diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index 0cd3fb70f..4884d469b 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -1335,6 +1335,7 @@ export type Mutation = { notify?: Maybe; /** Macro: Actions: Print PDF Asset */ printPdf?: Maybe; + clearPdf?: Maybe; commAddSignal?: Maybe; commUpdateSignal?: Maybe; /** @@ -4488,6 +4489,11 @@ export type MutationPrintPdfArgs = { }; +export type MutationClearPdfArgs = { + id: Scalars['ID']; +}; + + export type MutationCommAddSignalArgs = { id: Scalars['ID']; commSignalInput: CommSignalInput; @@ -6625,6 +6631,7 @@ export type Subscription = { shieldsUpdate?: Maybe>>; notify?: Maybe; widgetNotify?: Maybe; + printQueue?: Maybe>>; shortRangeCommUpdate?: Maybe>>; sickbayUpdate?: Maybe>>; signalJammersUpdate?: Maybe>>; @@ -7024,6 +7031,11 @@ export type SubscriptionWidgetNotifyArgs = { }; +export type SubscriptionPrintQueueArgs = { + simulatorId: Scalars['ID']; +}; + + export type SubscriptionShortRangeCommUpdateArgs = { simulatorId: Scalars['ID']; }; @@ -9234,6 +9246,14 @@ export enum NotifyColors { Dark = 'dark' } +export type PrintQueue = { + __typename?: 'PrintQueue'; + id: Scalars['ID']; + simulatorId: Scalars['String']; + asset: Scalars['String']; + timestamp: Scalars['Float']; +}; + export type ShortRangeComm = SystemInterface & { __typename?: 'ShortRangeComm'; id?: Maybe; @@ -11686,6 +11706,29 @@ export type UpdateLightingMutation = ( & Pick ); +export type ClearPdfMutationVariables = Exact<{ + id: Scalars['ID']; +}>; + + +export type ClearPdfMutation = ( + { __typename?: 'Mutation' } + & Pick +); + +export type PrintQueueSubscriptionVariables = Exact<{ + simulatorId: Scalars['ID']; +}>; + + +export type PrintQueueSubscription = ( + { __typename?: 'Subscription' } + & { printQueue?: Maybe + )>>> } +); + export type ReactorAckWingPowerMutationVariables = Exact<{ id: Scalars['ID']; wing: Scalars['String']; @@ -15577,6 +15620,28 @@ export function useUpdateLightingMutation(baseOptions?: ApolloReactHooks.Mutatio return ApolloReactHooks.useMutation(UpdateLightingDocument, baseOptions); } export type UpdateLightingMutationHookResult = ReturnType; +export const ClearPdfDocument = gql` + mutation ClearPdf($id: ID!) { + clearPdf(id: $id) +} + `; +export function useClearPdfMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(ClearPdfDocument, baseOptions); + } +export type ClearPdfMutationHookResult = ReturnType; +export const PrintQueueDocument = gql` + subscription PrintQueue($simulatorId: ID!) { + printQueue(simulatorId: $simulatorId) { + id + asset + timestamp + } +} + `; +export function usePrintQueueSubscription(baseOptions?: ApolloReactHooks.SubscriptionHookOptions) { + return ApolloReactHooks.useSubscription(PrintQueueDocument, baseOptions); + } +export type PrintQueueSubscriptionHookResult = ReturnType; export const ReactorAckWingPowerDocument = gql` mutation ReactorAckWingPower($id: ID!, $wing: String!, $ack: Boolean!) { reactorAckWingRequest(id: $id, wing: $wing, ack: $ack) diff --git a/src/helpers/triggerLocalMacros.ts b/src/helpers/triggerLocalMacros.ts index a84a6fde9..874e222f6 100644 --- a/src/helpers/triggerLocalMacros.ts +++ b/src/helpers/triggerLocalMacros.ts @@ -12,6 +12,9 @@ const localMacros: {[key: string]: Function | null} = { printJS({printable: `/assets${asset}`, type: "pdf"}); }, }; + +export const localMacrosList = Object.keys(localMacros); + export default function triggerLocalMacros(actions: Macro[]) { actions.forEach(action => { if (localMacros[action.event]) { diff --git a/src/schema.graphql b/src/schema.graphql index 34afa381c..292731ae6 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -871,6 +871,7 @@ type Mutation { """Macro: Actions: Print PDF Asset""" printPdf(asset: String!): String + clearPdf(id: ID!): String commAddSignal(id: ID!, commSignalInput: CommSignalInput!): String commUpdateSignal(id: ID!, commSignalInput: CommSignalInput!): String @@ -1464,6 +1465,7 @@ type Subscription { shieldsUpdate(simulatorId: ID): [Shield] notify(simulatorId: ID!, station: String, trigger: String): Notification widgetNotify(simulatorId: ID!, station: String): String + printQueue(simulatorId: ID!): [PrintQueue] shortRangeCommUpdate(simulatorId: ID!): [ShortRangeComm] sickbayUpdate(simulatorId: ID): [Sickbay] signalJammersUpdate(simulatorId: ID!): [SignalJammer] @@ -3406,6 +3408,13 @@ enum NotifyColors { dark } +type PrintQueue { + id: ID! + simulatorId: String! + asset: String! + timestamp: Float! +} + type ShortRangeComm implements SystemInterface { id: ID simulatorId: ID