Initialization

quip.apps.InitOptions

This is the set of initialization options the app can specify when initializing the app instance.

initializationCallback: function(Element, quip.apps.InitializationParameters) [required]
Function that will be called to initialize the app's view once the app instance has received all of its stored data.

menuCommands: Array<quip.apps.MenuCommand> [optional]
List of quip.apps.MenuCommands that can be used within the toolbar menus and context menus.

toolbarCommandIds: Array<string> [optional]
List of command IDs that should be used as the top-level toolbar buttons. quip.apps.DocumentMenuCommands.MENU_MAIN will automatically be used as the first button in the toolbar.

Example

class Root extends React.Component {
    render() {
        return <div>Hello World!</div>
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        const rootRecord = quip.apps.getRootRecord();

        if (params.isCreation) {
            console.log("App instance created for the first time!")
        }

        ReactDOM.render(<Root />, root);
    },
    toolbarCommandIds: [
        "addItem",
        "deleteItem"
    ],
    menuCommands: [
        {
            id: "addItem",
            label: "Add Item",
            handler: () => console.log("Add Item"),
        },
        {
            id: "deleteItem",
            label: "Delete Item",
            handler: () => console.log("Delete Item"),
        },
    ],
});
quip.apps.InitializationParameters

This set of parameters is passed to the initializationCallback function that the app registers and allows the app to initialize the data for the app appropriately.

isCreation: boolean
Whether this is the first time this app instance is being rendered after creation

creationUrl: (string|undefined)
If this app instance was created from a URL paste in the document, this is the pasted URL

quip.apps.initialize

This is the API call that initializes the environment for this app instance. This call allows the app to register menu commands for the toolbar and register a callback function that will be called once the data for the app instance has been sent over.

@param {quip.apps.InitOptions} initOptions


quip.apps.MenuCommand

This represents a menu command that can be used as a button in the menu toolbar, as a menu option in a submenu within the toolbar, or as an option within a context menu.

id: string [required]
Must be unique. Can be one of quip.apps.DocumentMenuCommands.

label: string [optional]
String label displayed for the command

sublabel: string [optional]
String sublabel that is displayed for the command. Sublabel is right-aligned and rendered in gray text.

handler: function(string, Object, ?) [optional]
Handler function that is called when this command is selected. Should not be populated for commands that have isHeader: true or have subCommands.

isHeader: boolean [optional, default=False]
Whether or not this should be displayed as a header within the menu

subCommands: Array<string> [optional]
List of menu options to be displayed as a nested menu or submenu when this command is selected.

actionId: string [optional]
String representing a special action that should be taken by the host upon selection of this command. Should be one of quip.apps.DocumentMenuActions

actionParams: Object [optional]
Dictionary of parameters specific to the actionId.

actionStarted: function() [optional]
Callback function that is called when this action is started. For the SHOW_FILE_PICKER action, this is useful for showing uploading state.

quip.apps.DocumentMenuCommands

These constants are references to the default menus shown for the app as well as utilities like the menu separator, or built-in commands that are supported by the host.

MENU_MAIN: "DocumentMenu-main"
Default first toolbar button shown for the app

SEPARATOR: "DocumentCommand-separator"
Allows the developer to include a separator either within a submenu or to separate toolbar button groups

COPY_ANCHOR_LINK: "DocumentCommand-copyAnchorLink"
Default command supported by the host that will copy an anchor link to this app instance to the user's clipboard

DELETE_APP: "DocumentCommand-deleteApp"
Default command supported by the host that will delete this instance of the app

quip.apps.DocumentMenuActions

These constants are for special actions that can be hooked up to MenuCommands.

SHOW_FILE_PICKER: 1


quip.apps.ToolbarState

menuCommands: Array<quip.apps.MenuCommand> [optional]
List of quip.apps.MenuCommand objects to register with the host. If any of the IDs have been previously registered, this configuration will override the previous configuration.

toolbarCommandIds: Array<string> [optional]
List of command IDs to show as the app toolbar buttons. If not specified, the app retains the existing list of toolbar buttons.

disabledCommandIds: Array<string> [optional]
List of command IDs that should be shown as disabled in toolbar menus. If not specified, the app retains the existing list of disabled command IDs.

highlightedCommandIds: Array<string> [optional]
List of command IDs that should be shown as highlighted in toolbar menus. If not specified, the app retains the existing list of highlighted command IDs.

Example

class Root extends React.Component {
    addCommands = () => {
        quip.apps.updateToolbar({
            toolbarCommandIds: [
                "addRow",
                "deleteRow",
            ],
            menuCommands: [
                {
                    id: "addRow",
                    label: "Add Row",
                    handler: () => console.log("Add row"),
                },
                {
                    id: "deleteRow",
                    label: "Delete Row",
                    handler: () => console.log("Delete row"),
                },
            ],
        });
    }

    disableCommand = () => {
        quip.apps.updateToolbar({
            disabledCommandIds: ["addRow"],
        });
    }

    render() {
        return (
            <div>
                <button onClick={this.addCommands}>Add commands</button>
                <button onClick={this.disableCommand}>Disable command</button>
            </div>
        );
    }
}

// App is initialized with no toolbar
quip.apps.initialize({
    initializationCallback: (root, params) => {
        ReactDOM.render(<Root />, root);
    },
});
quip.apps.updateToolbar

Sends a message to the host to update various attributes of the menu commands available in the toolbar. This can update the configuration of the commands, which commands appear as top-level buttons in the toolbar, and also designate which commands should be disabled or highlighted in the menus.

@param {!quip.apps.ToolbarState} toolbarState [required]


quip.apps.showContextMenu

Sends a message to the host to show a context menu with the given set of commandIds.

@param {!Event} event [required]
used to determine the location of where to show the context menu

@param {!Array<string>} commandIds [required]
List of previously registered command IDs that should be shown as the options for this context menu.

@param {Array<string>=} highlightedCommandIds [optional]
List of commandIds that should be shown as highlighted in this context menu.

@param {Array<string>=} disabledCommandIds [optional]
List of commandIds that should be shown as disabled in this context menu.

@param {function (): ?=} onDismiss [optional]
Callback function for when the context menu is dismissed (either due to the user selecting a command or clicking away)

@param {Object=} contextArg [optional]
Object that can be passed through to the handler for the command that the user selects.

Example

class MyComponent extends React.Component {
    state = {
        canAdd: true,
        canDelete: false,
    }

    handleClick = (e) => {
        const { canAdd, canDelete } = this.state;
        const disabledCommands = [];

        if (!canAdd) disabledCommands.push("addItem");
        if (!canDelete) disabledCommands.push("deleteItem");

        quip.apps.showContextMenu(
            e,
            [
                "addItem", // id from menuCommand in Initialization
                "deleteItem", // id from menuCommand in Initialization,
                quip.apps.DocumentMenuCommands.SEPARATOR,
                quip.apps.DocumentMenuCommands.DELETE_APP,
            ],
            [], // No highlighted commands
            disabledCommands, // Disabled commands based on state
            () => console.log("Context menu has been closed"),
        );
    }

    render() {
        return <div onClick={this.handleClick}>Show Context Menu</div>;
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        ReactDOM.render(<MyComponent />, root);
    },
    menuCommands: [
        {
            id: "addItem",
            label: "Add Item",
            handler: () => console.log("Add item called"),
        },
        {
            id: "deleteItem",
            label: "Delete Item",
            handler: () => console.log("Delete item called"),
        },
    ],
});
quip.apps.showContextMenuFromButton

Like quip.apps.showContextMenu, but allows the caller to specify a DOM node at which to anchor the context menu.

@param {!Node} button

@param {!Array<string>} commandIds

@param {Array<string>=} highlightedCommandIds

@param {Array<string>=} disabledCommandIds

@param {function (): ?=} onDismiss

@param {Object=} contextArg

Example

class MyComponent extends React.Component {
    handleClick = (e) => {
        quip.apps.showContextMenuFromButton(
            this.refs["button"], // ref to the button DOM element
            [
                "addItem", // id from menuCommand in Initialization
                "deleteItem", // id from menuCommand in Initialization,
                quip.apps.DocumentMenuCommands.SEPARATOR,
                quip.apps.DocumentMenuCommands.DELETE_APP,
            ],
            [], // No highlighted commands
            [], // No disabled commands
            () => console.log("Context menu has been closed"),
        );
    }

    render() {
        return <button ref="button" onClick={this.handleClick}>
            Show Context Menu From Button
        </button>;
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        ReactDOM.render(<MyComponent />, root);
    },
    menuCommands: [
        {
            id: "addItem",
            label: "Add Item",
            handler: () => console.log("Add item called"),
        },
        {
            id: "deleteItem",
            label: "Delete Item",
            handler: () => console.log("Delete item called"),
        },
    ],
});

Environment

Client Environment

quip.apps.isMobile

Returns whether this app is being rendered in a native mobile app context.

@return {boolean}

quip.apps.isNative

Returns whether this app is being rendered in a native app context (desktop or mobile).

@return {boolean}

quip.apps.isDesktopWeb

Returns whether this app is being rendered in a desktop web browser.

@return {boolean}

quip.apps.isIOs

Returns whether this app is being rendered in the native iOS app.

@return {boolean}

quip.apps.isAndroid

Returns whether this app is being rendered in the native Android app.

@return {boolean}

quip.apps.isWindows

Returns whether this app is being rendered in the native Windows app.

@return {boolean}

quip.apps.isMac

Returns whether this app is being rendered in the native Mac app.

@return {boolean}

quip.apps.isOnline

Returns a best-effort estimate of whether the user's client currently has an Internet connection.

@return {boolean}

Relevant Events

quip.apps.EventType.ONLINE_STATUS_CHANGED

quip.apps.getApiVersion

Returns a string representation of the currently running apps API version.

@return {string}

quip.apps.isApiVersionAtLeast

Returns whether the currently running apps API version is greater than or equal to the given version string.

@param {!string} version [required]

@return {boolean}


Getting Viewing User and Document Members

quip.apps.getViewingUser

Returns the quip.apps.User object representing the current viewing Quip user. Returns null if the viewer is not logged in to Quip.

@return {(quip.apps.User|null)}

Example

const listMembers = () => {
    const viewingUser = quip.apps.getViewingUser();

    const allMembers = quip.apps.getDocumentMembers();

    console.log(viewingUser.getName() + " is currently viewing the document");

    allMembers.forEach(user => console.log(user.getName() + " is a member of this document"));
}

quip.apps.addEventListener(quip.apps.EventType.DOCUMENT_MEMBERS_LOADED, listMembers);
quip.apps.isViewerLoggedIn

Returns whether or not the viewer is logged in to Quip.

@return {boolean}

quip.apps.getDocumentMembers

Returns an array of quip.apps.User objects representing the Quip users that are members of this thread.

@return {!Array<!quip.apps.User>}

Relevant Events

quip.apps.EventType.DOCUMENT_MEMBERS_LOADED


Document Environment

quip.apps.getQuipAppId

Returns the ID representing this app in the Quip namespace. Only call this if you need a globally unique ID to store outside of Quip. For all other uses, please use quip.apps.getRootRecordId instead.

@return {!string}

quip.apps.isAppFocused

Returns whether or not this app is currently focused in the document.

@return {boolean}

Relevant Events

quip.apps.EventType.FOCUS

quip.apps.EventType.BLUR

quip.apps.getThreadId

Returns the id of the thread containing this app.

@return {string}

quip.apps.isDocumentEditable

Returns whether or not the document containing this app is editable.

@return {boolean}

Relevant Events

quip.apps.EventType.DOCUMENT_EDITABLE_CHANGED

quip.apps.getContainerWidth

Returns the width of the app container, in pixels.

@return {number}

Relevant Events

quip.apps.EventType.CONTAINER_SIZE_UPDATE


Subscribing to Events

quip.apps.EventType

BLUR
Event when the user switches focus away from this app instance in the document.

FOCUS
Event when the user switches focus to this app instance in the document.

CONTAINER_SIZE_UPDATE
Triggers when the container size for this app instance changes.

USER_PREFERENCE_UPDATE
Triggers when the UserPreferences for the viewing user are updated.

SITE_PREFERENCE_UPDATE
Triggers when the SitePreferences for the viewing user's site are updated.

DOCUMENT_MEMBERS_LOADED
Triggers when the list of document members for the current document have been loaded and are available for querying.

DOCUMENT_EDITABLE_CHANGED
Event when the editable status of the document has changed.

ONLINE_STATUS_CHANGED
Event when the user's client's online status has changed.

Example

class FocusComponent extends React.Component {
    state = {
        focused: false,
    }

    componentDidMount() {
        quip.apps.addEventListener(quip.apps.EventType.FOCUS, this.onFocus);
        quip.apps.addEventListener(quip.apps.EventType.BLUR, this.onBlur);
    }

    componentWillUnmount() {
        quip.apps.removeEventListener(quip.apps.EventType.FOCUS, this.onFocus);
        quip.apps.removeEventListener(quip.apps.EventType.BLUR, this.onBlur);
    }

    onFocus = () => this.setState({ focused: true })

    onBlur = () => this.setState({ focused: false })

    render() {
        return <div>focused {this.state.focused}</div>;
    }
}
quip.apps.addEventListener

Subscribes a listener function to the given event type.

@param {!quip.apps.EventType} type [required]

@param {!function (): ?} listener [required]
Callback function that will be called when an event of this type happens.

quip.apps.removeEventListener

Unsubscribes a listener function from the given event type.

@param {!quip.apps.EventType} type [required]

@param {!function (): ?} listener [required]
Callback function that should no longer be called when an event of this type happens.


Special Actions

quip.apps.openLink

Opens the given URL in a new browser window.

@param {!string} url [required]

Example

class MyComponent extends React.Component {
    handleClick = (e) => {
        quip.apps.openLink("https://www.quip.com");
    }

    render() {
        return <button onClick={this.handleClick}>
            Open Link
        </button>;
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        ReactDOM.render(<MyComponent />, root);
    },
});

Sending a message

quip.apps.sendMessage

Creates a new message to be displayed in the document activity log. The message will be written from the current viewing user, will have the given text string, and will be attributed to this app.

@param {!string} text [required]

Example

class MyComponent extends React.Component {
    handleClick = (e) => {
        const message = "Sample Message";
        quip.apps.sendMessage(message);
    }

    render() {
        return <button onClick={this.handleClick}>
            Send Message
        </button>;
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        ReactDOM.render(<MyComponent />, root);
    },
});

Showing UI beyond the app boundaries

quip.apps.addDetachedNode

Register a node as being "detached" from the app container and thus possibly extending outside of it. The host will make a best effort to still display such nodes and allow user interaction with them when the app has focus (but may disallow it if it presents a click-jacking risk).

@param {!Node} node [required]

Example

class MyComponent extends React.Component {
    addNode = (e) => {
        quip.apps.addDetachedNode(this.refs["button"]);
    }

    removeNode = (e) => {
        quip.apps.removeDetachedNode(this.refs["button"]);
    }

    handleClick = (e) => {
        console.log("Button clicked!");
    }

    render() {
        // The style attributes will force the DOM element
        // to be rendered outside the container of the live app.
        // Hence it will not be user interactable unless it is
        // added as a detached node.
        const style = {
            position: "absolute",
            top: "100%",
            left: 0,
        };
        return <div>
            <button onClick={this.addNode}>
                Add Detached Node
            </button>
            <button onClick={this.removeNode}>
                Remove Detached Node
            </button>
            <button
                ref="button"
                style={style}
                onClick={this.handleClick}>
                    Click Me!
            </button>
        </div>;
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        ReactDOM.render(<MyComponent />, root);
    },
});
quip.apps.removeDetachedNode

Removes a previously-registered detached node.

@param {!Node} node [required]


Showing Dialogs

quip.apps.showBackdrop

Displays a backdrop over the document canvas that dims the document and app in order to allow the developer to render some UI that appears like a dialog. Takes an optional onDismiss callback that will be called when the backdrop is dismissed, e.g. through a user click.

@param {function (): ?=} onDismiss [optional]

Example

class MyDialogComponent extends React.Component {
    static propTypes = {
        children: React.PropTypes.element.isRequired,
        onDismiss: React.PropTypes.func,
    };

    componentDidMount() {
        quip.apps.showBackdrop(this.props.onDismiss);
        quip.apps.addDetachedNode(this.refs["node"]);
    }

    componentWillUnmount() {
        quip.apps.dismissBackdrop();
        quip.apps.removeDetachedNode(this.refs["node"]);
    }

    render() {
        const dimensions = quip.apps.getCurrentDimensions();
        const style = {
            position: "absolute",
            width: dimensions.width,
            height: dimensions.height,
            display: "flex",
            justifyContent: "center",
            alignItems: "center",
            backgroundColor: quip.apps.ui.ColorMap.BLUE.VALUE_LIGHT,
            color: quip.apps.ui.ColorMap.BLUE.VALUE,
            zIndex: 301,
        };
        return <div ref="node" style={style}>
            {this.props.children}
        </div>;
    }
}

class MyComponent extends React.Component {
    state = {
        showDialog: false
    }

    handleSubmit = (e) => {
        const value = this.refs["text"].value;
        if (value.length > 0) {
            this.setState({showDialog: true});
        }
    }

    onDialogDismiss = () => {
        this.setState({showDialog: false});
    }

    render() {
        let dialog;
        if (this.state.showDialog) {
            const dialogChildren = <span>Dialog!</span>;
            dialog = <MyDialogComponent
                onDismiss={this.onDialogDismiss}
                children={dialogChildren}/>;
        }
        return <div>
            <input ref="text" type="text"></input>
            <button onClick={this.handleSubmit}>Submit</button>
            {dialog}
        </div>;
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        ReactDOM.render(<MyComponent />, root);
    },
});
quip.apps.dismissBackdrop

Manually requests that the backdrop be dismissed. Allows the developer to pass skipCallback, which would avoid calling the onDismiss callback registered when showing the backdrop.

@param {boolean=} skipCallback [optional, default=False]


Sizing and Resizing

quip.apps.getCurrentDimensions

Returns the current width and height of this app instance.

@return {{width: number, height: number}}

quip.apps.forceUpdateDimensions

Forces an immediate update of the app's dimensions. Updates will automatically happen in response to the app's DOM mutations, so this should only be used in special cases where the update needs to happen immediately.

quip.apps.setWidthAndAspectRatio

Only available to apps with "scale" sizing. Tells the container the desired width and aspect ratio of the app. The width is saved, but does not affect the view since users are able to resize scale-sizing apps with columns. Passing along an updated aspect ratio (height / width) is critical if your app wants to change its shape. Otherwise, the container could improperly cutoff content or take up too much space in the document.

@param {number} width [required]

@param {number=} aspectRatio [optional]

quip.apps.enableResizing

Shows a drag handle for resizing when the app is focused. Resizing can only be enabled for apps with "scale" sizing.

Example

class MyComponent extends React.Component {

    componentDidMount() {
        quip.apps.enableResizing();
    }

    render() {
        return <div>Resize Me!</div>;
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        ReactDOM.render(<MyComponent />, root);
    },
});
quip.apps.disableResizing

Hides the drag handle shown from quip.apps.enableResizing(). Has no effect if quip.apps.enableResizing() has not been called.


Embedded iframes

quip.apps.registerEmbeddedIframe

Registers the given iframe for the purposes of automatically handling focus events on the app.

@param {!Node} iframe [required]

quip.apps.clearEmbeddedIframe

Clears any iframes previously registered with quip.apps.registerEmbeddedIframe


Record

A quip.apps.Record object is the basic unit of data storage in our API and represents a discrete unit of data within an app instance. By default, an app instance is created with a single quip.apps.RootRecord. The app may create additional quip.apps.Record instances to store discrete pieces of data that can be synced independently and can represent specific components within the app, such as a rich text box, an image, etc.

quip.apps.Record objects can store custom data for the app in the form of properties. Property data types can be any one of the following: a Javascript primitive (string, number, boolean, etc), a Javascript object or array that contains only primitives, a Record, or a list of Record objects, in the form of a quip.apps.RecordList. Record objects can be thought of as being analogous to JSON objects, with the primary difference being that defining a Record allows the developer to a) get independent syncing/merge behavior, b) add validation for their property data types and c) make use of our special data types, like quip.apps.RichTextRecord and quip.apps.ImageRecord.

Every Record or RecordList object is “owned” by another Record, in a hierarchical structure starting from the root. The developer does not need to explicitly specify these parent-child relationships; they are implicit upon creation of a Record or RecordList as a property of another Record.

Registration

All subclasses of quip.apps.Record defined by the developer must be registered with the apps API. The registration API is described here, along with the static methods and fields on the class that the developer can include in order to define the behavior of their quip.apps.Record class.

quip.apps.registerClass

Registers a subclass of quip.apps.Record (or RootRecord|ImageRecord|RichTextRecord) for use in the apps API. recordKey is required for all calls except for those registering subclasses of quip.apps.RootRecord. recordKey should be a string that is unique within the app namespace. It is stored in the Record data so that we can consistently instantiate the correct class for a stored Record object.

All registerClass() calls must be made before the quip.apps.initialize() call.

@param {!Function} recordClass

@param {string=} recordKey

Example

class MyRecord extends quip.apps.RootRecord {
    static getProperties = () => ({
        date: "number",
    })
}

quip.apps.registerClass(Root, "my-record");

class Root extends quip.apps.RootRecord {
    static getProperties = () => ({
        date: "number",
        myRecord: MyRecord,
    })
}

quip.apps.registerClass(Root, "root");

Defining a Record Class

Developers can configure their quip.apps.Record subclasses by defining these static methods/fields on their class. One in particular, getProperties(), is required, as it describes the property schema for their Record and is used both to validate the property values that are being stored for the Record object as well as to construct the right types of Record and RecordList objects when setting property values.

The API is relatively strict about setting new property values on Record objects; if the property name has not been defined by getProperties() or if the value does not match the data type defined in getProperties(), the set() call will fail. In addition to the properties that the developer defines in getProperties(), subclasses inheriting from Quip's special Record types (e.g. quip.apps.RichTextRecord or quip.apps.ImageRecord) can also inherit type-specific properties. These properties will be listed for each of the subclasses below, but the property names will be prefixed by the Record type (e.g. RichText_defaultText). Developers will also not be allowed to define property names with these prefixes to avoid collisions.

Special Record types:

Example

class MyRecord extends quip.apps.Record {
    static getProperties = () => ({
        title: "string",
        count: "number",
    })
}

quip.apps.registerClass(MyRecord, "my-record");

class RootRecord extends quip.apps.RootRecord {
    static getProperties = () => ({
        title: "string",
        items: "array",
        editable: "boolean",
        text: quip.apps.RichTextRecord,
        subRecord: MyRecord,
    })

    static getDefaultProperties = () => ({
        items: [],
        editable: true,
        text: { RichText_placeholderText: "Type In Here!" },
    })
}

quip.apps.registerClass(RootRecord, "root-record");

class Root extends React.Component {
    setTitle = () => {
        const { record } = this.props;
        record.set("title", "new title");
    }

    addItem = () => {
        const { record } = this.props;
        const current = record.get("items");
        record.set("items", current.concat(`new item ${current.length + 1}`));
    }

    toggleEditable = () => {
        const { record } = this.props;
        record.set("editable", !record.get("editable"));
    }

    addRecordData = () => {
        const { record } = this.props;
        record.set("subRecord", { title: "title", count: 5 });
    }

    render() {
        const { record } = this.props;
        return (
            <div>
                <button onClick={this.setTitle}>Set title</button>
                <button onClick={this.addItem}>Add item</button>
                <button onClick={this.toggleEditable()}>Toggle editable</button>
                <button onClick={this.addRecordData}>Add record data</button>
                <br />
                {record.get("title") || "No title set"}
                <br />
                {record.get("items").join(", ") || "No items set"}
                <br />
                {record.get("editable") ? "editable" : "not editable"}
                <br />
                {record.get("subRecord")
                    ? JSON.stringify(record.get("subRecord").getData())
                    : "no subrecord data"
                }
                <br />
                <quip.apps.ui.RichTextBox record={record.get("text")} />
            </div>
        );
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        ReactDOM.render(<Root record={quip.apps.getRootRecord()} />, root);
    },
});

Properties and Data

quip.apps.Record.getProperties

Required static method on all classes registered using quip.apps.registerClass(). This should return a "schema" of the properties that will be stored for this Record type, in the following format:

{
  $PROPERTY_NAME1: "string"|"number"|"boolean"|"object"|"array"|Function|quip.apps.RecordList.Type,
  $PROPERTY_NAME2: ...,
  ...
}

The value for each property should indicate the data type of the property being stored, and can either be one of a list of hard-coded primitive types, a Record constructor function (e.g. quip.apps.ImageRecord, quip.apps.RichTextRecord, or a Record subclass that the developer has defined), or a list type generated using quip.apps.RecordList.Type().

Example:

static getProperties() {
  return {
    "header": CardRecord,
    "cards": quip.apps.RecordList.Type(CardRecord),
    "color": "string"
  };
}

@return {!Object<string, (string|Function)>}

quip.apps.Record.getDefaultProperties

Optional static method that returns default values for some or all of the properties defined on this Record.

@return {Object<string, *>}

quip.apps.Record.prototype.get

Returns the stored property value for this key. If a value has not yet been set but this property has a value in getDefaultProperties(), this will set the default value from getDefaultProperties() and return it.

@param {string} key

@return {*}

quip.apps.Record.prototype.has

Returns true if this Record has a value stored for this property.

@param {string} key

@return {boolean}

quip.apps.Record.prototype.getData

Returns a dictionary of all the property values stored in the Record.

@return {!Object}

quip.apps.Record.prototype.set

This sets the value for a property on the given Record. We use the property schema to determine whether we should create a Record or RecordList, and if we do, what type of Record to create.

When setting a property that's of type Record or RecordList, if the property already has a stored value, this will throw an error. Developers need to clear the property before resetting it to a new value.

This returns the Record whose property is being set to enable chaining of setters.

@param {!string} propertyName [required]

@param {*} value [required]

@return {quip.apps.Record}

quip.apps.Record.prototype.clear

Clears a property on the Record. If the property is a Record or a RecordList, then the Record/RecordList will be deleted, unless skipDelete is true. If skipDelete is true, then we will not call delete on the Record/RecordList and will return a reference to it. This is intended to be used by developers to move a Record property from one object to another.

@param {string} key [required]

@param {boolean=} skipDelete [optional]

@return {quip.apps.Record|quip.apps.RecordList|null}

quip.apps.Record.prototype.clearData

Clears all properties for the given Record.


Getting a Record

quip.apps.getRecordById

Gets a quip.apps.Record by ID. Returns null if no Record exists for that ID.

@param {!string} id [required]

@return {(quip.apps.Record|null)}

quip.apps.getRootRecord

Gets a pointer to the RootRecord for this app instance.

@return {!quip.apps.RootRecord}

quip.apps.getRootRecordId

Gets the ID of the RootRecord for this app instance.

@return {!string}


Basic APIs

quip.apps.Record.prototype.getId

Returns the ID for this Record. This ID is guaranteed to be unique within this app instance, but if you are planning to store this ID in a datastore external to Quip, please use quip.apps.Record.prototype.getUniqueId instead.

@return {string}

quip.apps.Record.prototype.getUniqueId

Returns an ID for this Record that is safe to store in an external datastore. For Record references that are stored with Quip, please use quip.apps.Record.prototype.getId instead.

@return {string}

quip.apps.Record.prototype.isDeleted

Returns whether or not this Record instance has been deleted.

@return {boolean}

quip.apps.Record.prototype.delete

Sets this Record as deleted. If this Record is in a RecordList, it will be removed from the list. All Record and RecordList properties of this Record are also deleted.

quip.apps.Record.prototype.initialize

Override this method to do initialization for this Record instance upon instantiation.

quip.apps.Record.prototype.listen

Subscribe to changes to this Record. The listener function will be called for both local and remote changes to this Record.

@param {function (!quip.apps.Record): ?} listener

quip.apps.Record.prototype.unlisten

Unsubscribe from changes to this Record.

@param {function (!quip.apps.Record): ?} listener


Data Version

quip.apps.Record.DATA_VERSION

Optional static field that defines the data version that new Records of this type should be created with.

quip.apps.Record.prototype.getDataVersion

Returns a number representing the version for the data schema this Record was written with. Can be used to help the developer differentiate between Record data written with different versions of code. Reflects the data version specified on the class at Record creation or through calls to setDataVersion(). If a data version was unspecified, this returns 0.

@return {number}

quip.apps.Record.prototype.setDataVersion

Updates the data version associated with this Record instance.

@param {!number} version [required]


quip.apps.Record.prototype.getParentRecord

Returns the Record object that contains either this Record as a property or, if this Record lives within a RecordList, returns the Record containing this RecordList as a property.

Only returns null for the RootRecord.

@return {(quip.apps.Record|null)}

quip.apps.Record.prototype.getPreviousSibling

Returns the sibling Record that precedes this one if this Record is in a RecordList. Returns null if this Record is not in a RecordList or is the first Record in the list.

@return {(quip.apps.Record|null)}

quip.apps.Record.prototype.getNextSibling

Returns the sibling Record that follows this one if this Record is in a RecordList. Returns null if this Record is not in a RecordList or is the last Record in the list.

@return {(quip.apps.Record|null)}

quip.apps.Record.prototype.getContainingList

If this Record is contained in a RecordList, this returns the RecordList.

@return {(quip.apps.RecordList|null)}

Comments

quip.apps.Record.prototype.supportsComments

Override this to return whether this Record supports commenting. By default, this returns false.

Note that this has no effect when overridden in the RootRecord. In order to control whether commenting is available on the app as a whole, please see the disable_app_level_comments field in the app manifest.

@return {boolean}

Example

class RootRecord extends quip.apps.RootRecord {
    static getProperties = () => ({
        commentableRecord: CommentableRecord,
    })

    static getDefaultProperties = () => ({
        commentableRecord: {},
    })
}

quip.apps.registerClass(RootRecord, "root-record");

class CommentableRecord extends quip.apps.Record {
    static getProperties = () => ({})

    getDom() {
        return this.node;
    }

    setDom(node) {
        this.node = node;
    }

    supportsComments() {
        return true;
    }
}

quip.apps.registerClass(CommentableRecord, "commentable-record");

class Root extends React.Component {
    render() {
        const commentableRecord = quip.apps.getRootRecord().get("commentableRecord");
        return (
            <div ref={(c) => commentableRecord.setDom(c)}>
                Comment
                <quip.apps.ui.CommentsTrigger
                    record={commentableRecord}
                    showEmpty={true}
                />
            </div>
        );
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        ReactDOM.render(<Root />, root);
    },
});
quip.apps.Record.prototype.getDom

Override this method to return the DOM node that represents this Record. This is required if this Record supports comments, as this DOM node will be used to highlight the Record when showing the corresponding comments.

@return {!Node}

quip.apps.showComments

Sends a message to the host to show the comments UI for the given Record ID.

@param {!string} id [required]

quip.apps.Record.prototype.listenToComments

Subscribe to changes in the comments associated with this Record ID.

@param {function (!quip.apps.Record): ?} listener

quip.apps.Record.prototype.unlistenToComments

Unsubscribe from changes to the comments associated with this Record.

@param {function (!quip.apps.Record): ?} listener


RecordList

Type Declaration

quip.apps.RecordList.Type

Used to declare a property type that is a RecordList containing a particular type of Record.

@param {!Function} recordConstructor [required]
Class name for the Record type that this RecordList contains

@return {!quip.apps.Record.Type}

Example

class MyRecord extends quip.apps.Record {
    static getProperties = () => ({
        title: "string",
        disabled: "boolean",
    })

    static getDefaultProperties = () => ({
        disabled: true,
    })
}

quip.apps.registerClass(MyRecord, "my-record");

class RootRecord extends quip.apps.RootRecord {
    static getProperties = () => ({
        myRecords: quip.apps.RecordList.Type(MyRecord),
    })

    static getDefaultProperties = () => ({
        myRecords: [],
    })
}

quip.apps.registerClass(RootRecord, "root-record");

class MyComponent extends React.Component {
    render() {
        const record = quip.apps.getRootRecord();
        return (
            <div>
                {record.get("myRecords").getRecords().map((r) => (
                    <div key={r.getId()}>
                        {r.get("title")}
                        {" - "}
                        {r.get("disabled") ? "disabled" : "active"}
                    </div>
                ))}
            </div>
        );
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        const rootRecord = quip.apps.getRootRecord();
        const myRecords = rootRecord.get("myRecords");
        if (params.isCreation) {
            myRecords.add({ title: "Title 2" });
            myRecords.add({ title: "Title 3" });
            myRecords.add({ title: "Title 1" }, 0);
            myRecords.get(0).set("disabled", false);
        }
        ReactDOM.render(<MyComponent />, root);
    },
});

Accessors

quip.apps.RecordList.prototype.getId

Returns the ID of this RecordList.

@return {string}

quip.apps.RecordList.prototype.get

Returns the Record at the given index in this list.

@param {!number} index [required]

@return {(quip.apps.Record|null)}

quip.apps.RecordList.prototype.getRecords

Returns an array of all the Record objects in this list.

@return {!Array<quip.apps.Record>}

quip.apps.RecordList.prototype.contains

Returns whether or not the given recordId is contained in this list.

@param {!string} recordId [required]

@return {boolean}

quip.apps.RecordList.prototype.count

Returns the number of Record objects in this list.

@return {number}

quip.apps.RecordList.prototype.indexOf

Returns the index of the given recordId in this list. Returns -1 if this recordId is not contained in this list.

@param {!string} recordId [required]

@return {number}

quip.apps.RecordList.prototype.isDeleted

@return {boolean}


Mutators

quip.apps.RecordList.prototype.add

This creates a new Record and adds it to this RecordList. We use the list type definition to determine what type of Record to create from the fields in the value. If index is not specified, the Record is appended to the end of the list.

Returns the newly created Record.

@param {!Object} value [required]

@param {number=} index [optional]

@return {!quip.apps.Record}

quip.apps.RecordList.prototype.move

Moves the given record to a new index in the list. If record is not currently in this list, it will add this Record to the list (and remove it from the list it currently lives in, if there is one). If record is currently the property of another Record object, this returns false and does not execute the move.

@param {!quip.apps.Record} record [required]

@param {!number} index [required]

@return {boolean}

quip.apps.RecordList.prototype.remove

Removes the given Record from this list. Deletes the removed record unless skipDelete is true.

@param {!quip.apps.Record} record [required]

@param {boolean=} skipDelete [optional]

@return {boolean}

quip.apps.RecordList.prototype.delete

Deletes this list. Also deletes all Record objects in this list.


Subscribe

quip.apps.RecordList.prototype.listen

Subscribes the listener function to changes in this RecordList. This listener function is only fired when the list of Records changes and not for changes in each Record; the developer is responsible for subscribing to changes in individual Records if needed.

@param {function ((quip.apps.RecordList|null)): ?} listener

@return {undefined}

quip.apps.RecordList.prototype.unlisten

Unsubscribes this listener function from changes in this RecordList.

@param {function ((quip.apps.RecordList|null)): ?} listener

@return {undefined}


RichTextRecord

A RichTextRecord is a special type of Record that corresponds to a rich text box within the app. The developer can create and delete RichTextRecord objects and can render them within their app by using the React component: <quip.apps.ui.RichTextBox/>.

Special Properties

{
// Optional. Allows the creator to specify the default text that should appear
// in the RichTextBox on creation.
    RichText_defaultText: "string",
// Optional. Allows the creator to specify  placeholder text that should
// appear in the RichTextBox whenever it is empty.
    RichText_placeholderText: "string",
}

Example

class DraggableCard extends quip.apps.RichTextRecord {
    static getProperties = () => ({
        color: "string",
    })
}

quip.apps.registerClass(DraggableCard, "draggable-card");

class RootRecord extends quip.apps.RootRecord {
    static getProperties = () => ({
        title: quip.apps.RichTextRecord,
        cards: quip.apps.RecordList.Type(DraggableCard),
    })

    static getDefaultProperties = () => ({
        title: { RichText_placeholderText: "Add a title" },
        cards: [],
    })
}

quip.apps.registerClass(RootRecord, "root");

class Root extends React.Component {
    render() {
        const record = quip.apps.getRootRecord();
        const title = record.get("title");
        const cards = record.get("cards").getRecords();
        return (
            <div>
                <quip.apps.ui.RichTextBox record={title} />
                {cards.map((card) => (
                    <quip.apps.ui.RichTextBox
                        key={card.getId()}
                        record={card}
                        color={card.get("color")}
                    />
                ))}
            </div>
        );
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        const rootRecord = quip.apps.getRootRecord();
        const cardList = rootRecord.get("cards");
        if (params.isCreation) {
            cardList.add({
                color: "BLUE",
                RichText_defaultText: "Blue card!"
            });
            cardList.add({
                color: "RED",
                RichText_defaultText: "Red card!"
            });
        }
        ReactDOM.render(<Root />, root);
    },
});

Content Access

quip.apps.RichTextRecord.prototype.getTextContent

Exports the current contents of the RichTextRecord to a Markdown string.

@return {string}

quip.apps.RichTextRecord.prototype.empty

Returns true if there is no text in this RichTextRecord.

@return {boolean}

quip.apps.RichTextRecord.prototype.listenToContent

Subscribes this listener function to any changes in the contents of this RichTextRecord.

@param {function(!quip.apps.RichTextRecord): ?} listener [required]

quip.apps.RichTextRecord.prototype.unlistenToContent

Unsubscribes this listener function from changes in the content of this RichTextRecord.

@param {function(!quip.apps.RichTextRecord): ?} listener [required]


Actions

quip.apps.RichTextRecord.prototype.focus

Sets focus to this RichTextRecord. fromPrevious allows you to configure whether the focus should be set at the beginning or the end of this RichTextRecord's content.

@param {boolean=} fromPrevious [optional, default=True]

quip.apps.RichTextRecord.prototype.selectAll

Sets the browser selection to select all content within this RichTextRecord.

quip.apps.RichTextRecord.prototype.replaceContent

Replaces the content of this RichTextRecord with the given text string. Requires that the RichTextRecord is actively mounted in a RichTextBox.


Constants

quip.apps.RichTextRecord.Style

Constants representing different rich text styles available in the RichTextBox. Can be used in the allowedStyles prop in quip.apps.ui.RichTextBox to specify which styles should be allowed in the RichTextBox.

@enum {number}
TEXT_PLAIN:1
TEXT_H1:2
TEXT_H2:3
TEXT_H3:4
TEXT_CODE:5
TEXT_BLOCKQUOTE:6
TEXT_PULL_QUOTE:7
LIST_BULLET:8
LIST_NUMBERED:9
LIST_CHECKLIST:10
HORIZONTAL_RULE:11
IMAGE:12


ImageRecord

An ImageRecord is a special type of Record that corresponds to an image rendered within the app. The developer can create and delete ImageRecord objects and can render them within their app by using the React component: <quip.apps.ui.Image/>.

Special Properties

{
// Required. Specifies the sizes (widths, in pixels) that Quip should save
// thumbnails for the uploaded image file.
    Image_requestedThumbnailWidths: "array"
}

Example

class CaptionedImageRecord extends quip.apps.Record {
    static getProperties = () => ({
        image: quip.apps.ImageRecord,
        caption: quip.apps.RichTextRecord,
    })

    static getDefaultProperties = () => ({
        image: {},
        caption: { RichText_placeholderText: "Add a caption" },
    })
}

quip.apps.registerClass(CaptionedImageRecord, "captioned-image");

class RootRecord extends quip.apps.RootRecord {
    static getProperties = () => ({
        captionedImage: CaptionedImageRecord,
    })

    static getDefaultProperties = () => ({
        captionedImage: {},
    })
}

quip.apps.registerClass(RootRecord, "root");

class ImageWithCaption extends React.Component {
    render() {
        const record = quip.apps.getRootRecord();
        const captionedImage = record.get("captionedImage");
        return (
            <div>
                <quip.apps.ui.Image
                    record={captionedImage.get("image")}
                    width={200}
                />
                <quip.apps.ui.RichTextBox
                    record={captionedImage.get("caption")}
                    allowImages={false}
                />
            </div>
        );
    }
}

quip.apps.initialize({
    initializationCallback: (root, params) => {
        ReactDOM.render(<ImageWithCaption />, root);
    },
});

APIs

quip.apps.ImageRecord.prototype.chooseFile

Prompts the user with a file picker to have them choose an image file. Once the file is successfully uploaded to Quip and is accessible, listeners for this Record will be notified.

quip.apps.ImageRecord.prototype.hasImage

Returns whether this ImageRecord has already been successfully associated with an image file.

@return {boolean}

quip.apps.ImageRecord.prototype.openInLightbox

Sends a message to the host to open this image in an external lightbox.

quip.apps.ImageRecord.prototype.downloadOriginal

Sends a message to the host to trigger a download of the original image file to the user's machine.

quip.apps.ImageRecord.prototype.handleShowFilePickerActionStarted

Convenience callback to allow an ImageRecord to handle the actionStarted phase for a MenuCommand that uses quip.apps.DocumentMenuActions.SHOW_FILE_PICKER.

quip.apps.ImageRecord.prototype.handleShowFilePickerAction

Convenience callback to allow an ImageRecord to handle the result for a MenuCommand that uses quip.apps.DocumentMenuActions.SHOW_FILE_PICKER.

@param {Array<Object>} blobsWithThumbnails


CanvasRecord

A CanvasRecord is a special type of Record that adds support for position-based comments. For example, the Image Live App uses this to allow comments to be added anywhere on the image. CanvasRecord can be treated as an opaque object for developers who pass it into a <quip.apps.ui.Canvas> React component.


CanvasRecord.CommentAnchorRecord

Represents the location of a comment anchor within a Canvas.

quip.apps.CanvasRecord.CommentAnchorRecord.prototype.getXFraction
quip.apps.CanvasRecord.CommentAnchorRecord.prototype.getYFraction

Returns the x- and y-coordinates of the comment anchor. These are in fraction of the canvas size, so 0.25 for getXFraction means a quarter of the width away from the left-edge & 0.99 for getYFraction means near the bottom.

@return {number}


User

A quip.apps.User is a representation of a Quip user for the app's use. They are immutable but allow the app to access and display some basic information about the user. To render a profile picture for the user, you can use <quip.apps.ui.ProfilePicture/>.


Getting a user

quip.apps.getUserById

Gets the quip.apps.User object for the given ID. Will only return data if the user is the viewing user or a document member. Returns null if no user with that ID was found.

@param {!string} userId [required]

@return {(quip.apps.User|null)}


User properties

quip.apps.User.prototype.getId

Returns the unique ID representing this user.

@return {string}

quip.apps.User.prototype.getName

Returns the user's name.

@return {string}

quip.apps.User.prototype.getFirstName

Returns the user's first name.

@return {string}


Blob

A quip.apps.Blob instance allows the app to store some binary data with Quip that can be accessed either via URL or as an ArrayBuffer. quip.apps.Blobs can be created either by passing in an ArrayBuffer of binary data or by showing a file picker to the user and allowing them to upload a file from their machine.


Creating a Blob

quip.apps.createBlobFromData

Creates a new Blob instance from the given data. If there’s a filename associated with this data, it can be passed in.

@param {!ArrayBuffer} data [required]
ArrayBuffer of data to create the blob from

@param {string=} filename [optional]
Filename to attach to the blob

@return {!quip.apps.Blob)}

quip.apps.BlobWithThumbnails

blob: quip.apps.Blob,

thumbnails: (Array<quip.apps.Blob>|undefined)

quip.apps.showFilePicker

Sends a message to the host to show a file picker to the user. The callback will be triggered after the file that the user has selected has been successfully uploaded to Quip and can be accessed via URL.

@param {function (): ?} onFilesPicked [required]
Callback function that will be triggered after the user has selected a file in the picker.

@param {function ((Array<quip.apps.BlobWithThumbnails>|null)): ?} onFilesUploaded [required]
Callback function that will be triggered after the file the user has selected has been successfully uploaded to Quip and can be accessed via URL.

@param {Array<string>=} allowedTypes [optional]
List of MIME types that the user is allowed to choose from in the file picker

@param {Array<number>=} requestedThumbnailWidths [optional]
List of thumbnail widths for Quip to generate from the selected file. If specified, you must also specify a list of allowedTypes that only includes image MIME types.


Blob API

quip.apps.Blob.prototype.getId

Returns the ID for this Blob. If this Blob has not yet been stored to Quip, this will return null.

@return {(null|string)}

quip.apps.Blob.prototype.hasLoadedData

Returns whether or not the data for this Blob has been loaded.

@return {boolean}

quip.apps.Blob.prototype.onDataLoaded

Schedules a callback function that will be triggered when the data for this Blob has been loaded from Quip. If the data has already been loaded, loadedCallback will be triggered immediately. Also takes an optional loadErrorCallback that will be triggered if an error is encountered when trying to perform the load.

@param {function (!quip.apps.Blob): ?} loadedCallback [required]

@param {function (!quip.apps.Blob): ?=} loadErrorCallback [optional]

quip.apps.Blob.prototype.getData

Returns the data for this Blob. If the data hasn't been loaded yet, this will return null.

@return {(ArrayBuffer|null)}

quip.apps.Blob.prototype.url

Returns the URL for this Blob. If the Blob has not been loaded yet, this will return null.

@return {(null|string)}

quip.apps.Blob.prototype.getFilename

Returns the filename associated with this Blob. If the Blob has not been loaded yet, this will return null.

@return {(null|string)}

quip.apps.Blob.prototype.downloadAsFile

Triggers a download in the browser for the contents of this Blob, as a file.

quip.apps.Blob.prototype.openInLightbox

Triggers the host document to open the given Blob in a lightbox. Only supported for Blobs containing image data.


Preferences

We allow the app to store a small amount of preference data on the viewing User or the viewing user's Site. The preferences are stored as a series of string keys and values. This storage is intended to allow apps to store data that should be tied to the Quip user rather than to any particular instance of the app.


Getting preferences

quip.apps.getUserPreferences

Returns the Preferences object that's scoped to the viewing User. Returns null if there is no viewing user.

@return {(quip.apps.Preferences|null)}

Relevant Events

quip.apps.EventType.USER_PREFERENCE_UPDATE

quip.apps.getSitePreferences

Returns the Preferences object that's scoped to the viewing User's Site. Returns null if there is no viewing user or no site associated with the user.

@return {(quip.apps.Preferences|null)}

Relevant Events

quip.apps.EventType.SITE_PREFERENCE_UPDATE


Preferences API

quip.apps.Preferences.prototype.getAll

Returns a dictionary of the preferences that have been stored.

@return {!Object<string,string>}

quip.apps.Preferences.prototype.getForKey

Returns the string value for the given preference key.

@param {!string} key [required]

@return {string}

quip.apps.Preferences.prototype.save

Saves the given set of preferences. Only keys that are included in this prefs dictionary are modified, any other keys that already exist on the user are preserved. In order to delete a preference key, the caller must include that key in the prefs object with an undefined value.

@param {!Object<string,string>} prefs [required]

quip.apps.Preferences.prototype.clear

Clears all stored preferences.


React UI Components

quip.apps.ui.RichTextBox

propTypes

record: React.PropTypes.instanceOf(quip.apps.RichTextRecord).isRequired
RichTextRecord object containing the data for this RichTextBox.

scrollable: React.PropTypes.bool [default=false]
Controls whether the RichTextBox will allow vertical scrolling once the contents have grown larger than the maxHeight. By default, the extra content tighter is hidden

width: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]) [default=300]
Width (px) for the RichTextBox. Can also be a string like "100%".

minHeight: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]) [default=20]
Minimum height (px) for the RichTextBox regardless of its contents Can also be a string like "100%".

maxHeight: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]) [default=300]
Maximum height (px) for the RichTextBox. If its contents grow past this height, the RichTextBox may become scrollable depending on the scrollable prop Can also be a string like "100%".

disableInlineMenus: React.PropTypes.bool [default=false]
Controls whether the default inline format menu and link inspector are shown

disableAutocomplete: React.PropTypes.bool [default=false]
Controls whether autocomplete is enabled in the RichTextBox

onComponentHeightChanged: React.PropTypes.func
Callback for when the height of this RichTextBox has changed

useDocumentTheme: React.PropTypes.bool [default=false]
Whether the contents of the RichTextBox should be rendered using the host document theme. If this is false, contents are rendered in Lightning theme

allowedStyles: React.PropTypes.arrayOf(quip.apps.RichTextRecord.Style) [default=[]]
If specified, this provides an exact list of the formatting styles that will be permitted to be created in this RichTextBox and will supersede any additional allow* props specified.

allowImages: React.PropTypes.bool [default=true]
Whether this RichTextBox allows insertion of images

allowLists: React.PropTypes.bool [default=true]
Whether this RichTextBox allows insertion of lists

allowHeadings: React.PropTypes.bool [default=false]
Whether this RichTextBox allows heading font styles

allowSpecialTextStyles: React.PropTypes.bool [default=false]
Whether this RichTextBox allows text styles like Code Block, Blockquote or Pull Quote

allowHorizontalRules: React.PropTypes.bool [default=false]
Whether this RichTextBox allows insertion of horizontal rules

maxListIndentationLevel: React.PropTypes.number
Sets a max indentation level for lists contained in this RichTextBox

readOnly: React.PropTypes.bool [default=false]
Whether this RichTextBox should be rendered in read-only mode

onFocus: React.PropTypes.func
Callback for when this RichTextBox gains focus

onBlur: React.PropTypes.func
Callback for when this RichTextBox loses focus

handleKeyEvent: React.PropTypes.func
Callback for keyboard events when this RichTextBox is focused. If handleKeyEvent returns true, the event is not passed to the RichTextBox to be processed

color: React.PropTypes.oneOf(Object.keys(quip.apps.ui.ColorMap))
Controls the color of the text and the cursor


quip.apps.ui.CommentsTrigger

propTypes

record: React.PropTypes.instanceOf(quip.apps.Record).isRequired,

showEmpty: React.PropTypes.bool [default=false]
Whether or not the CommentsTrigger should be rendered if there are no comments on the Record


quip.apps.ui.Image

propTypes

record: React.PropTypes.instanceOf(quip.apps.ImageRecord).isRequired,
ImageRecord containing the image data to be displayed.

minWidth: React.PropTypes.number [default=100]
Specifies a minimum width (px) for this image. Prevents resizing to smaller than this width.

defaultWidth: React.PropTypes.number
Specifies the default width (px)

width: React.PropTypes.number
height: React.PropTypes.number
These props specify the size (px) of the rendered image. If not specified, the image is rendered according to the size stored in the ImageRecord

borderRadius: React.PropTypes.number [default=0]
Specify a value between 0 and 0.5 to control how rounded the image corners should be. A value of 0 gives square corners, a value of 0.5 makes the image circular (if the image has the same width and height)

placeholderWidth: React.PropTypes.number [default=100]
placeholderHeight: React.PropTypes.number [default=100]
When the ImageRecord does not have an associated image file yet, these props control the size of the placeholder

allowResizing: React.PropTypes.bool [default=true]
Whether to allow the user to resize the image

onSizeChanged: React.PropTypes.func,
Callback for when the rendered size of the image has changed

onResizingModeChanged: React.PropTypes.func,
Callback for when the image enters and exits resizing mode

onEditingModeChanged: React.PropTypes.func,
Callback for when the image enters and exits editing mode

responsiveToContainerWidth: React.PropTypes.bool [default=false]
When set to true, the sizing of the image will respect the app container width. If the size of the image is wider than the container width, it will be capped at the container width and will not allow resizing to be wider than the container


quip.apps.ui.Canvas

propTypes

record: React.PropTypes.instanceOf(quip.apps.CanvasRecord).isRequired
CanvasRecord object that acts as the data store for the Canvas.

isInCommentMode: React.PropTypes.bool [default=false]
Whether the user is adding a CommentTrigger to the Canvas. This causes the Live App to show a comment bubble that follows the mouse. This behavior is only supported for Canvases that cover the entire Live App. Otherwise, use quip.apps.ui.Canvas.prototype.addCommentAtPoint.

onCommentAdd: React.PropTypes.func
Zero-argument callback when a CommentAnchor is added when isInCommentMode is true. You should pass false for isInCommentMode after this.

cancelCommentMode: React.PropTypes.func
Zero-argument callback when the user cancels out of adding a comment bubble from isInCommentMode true. You should pass false for isInCommentMode after this.

isCommentAnchorValidCallback: React.PropTypes.func
Callback used to flag comment anchors not in valid locations. This is used by <quip.apps.ui.Image/> to show which comment anchors will be removed if a crop is committed. Takes a quip.apps.ui.Canvas.CommentAnchorRecord object as an argument. Expects to have a boolean returned where false means to display the red, cross-out bubble. If not passed in, all comment anchors are assumed to be in valid locations.

quip.apps.ui.Canvas.prototype.addCommentAtPoint

Adds a CommentTrigger at the Event location.

@param {number} clientX
@param {number} clientY
These are the clientX & clientY taken from an Event


quip.apps.ui.ProfilePicture

propTypes

user: React.PropTypes.instanceOf(quip.apps.User).isRequired
quip.apps.User object to display the profile picture for

size: React.PropTypes.number.isRequired
Size in px that is used for the profile picture width and height

round: React.PropTypes.bool [default=false]
Whether to render the profile picture as round (defaults to square)

fallbackToInitials: React.PropTypes.bool [default=true]
If the user doesn't have a profile picture, we will render their initials as the profile picture if this is true, otherwise we will render the default placeholder profile picture


quip.apps.ui.CalendarPicker

propTypes

initialSelectedDateMs: React.PropTypes.number.isRequired
The initially selected date in the calendar, specified as milliseconds since 1/1/1970

onChangeSelectedDateMs: React.PropTypes.func.isRequired
Callback for when the user selects a new date. The function will be invoked with a single argument: the new selected date in milliseconds.


quip.apps.ui.Button

propTypes

text: React.PropTypes.string.isRequired
Text for label displayed on the button

primary: React.PropTypes.bool [default=false]
Gives the button different styling (blue background, white text)

disabled: React.PropTypes.bool [default=false]
Gives the button disabled styling

type: React.PropTypes.string
Sets the type attribute of this button (e.g. "submit")

onClick: React.PropTypes.func
onClick handler for the button


UI Colors

The map below should help make it easy to build card components in your app. In the Record, you should store the KEY value for colors and then look up the values in this map at render time - that way, if the colors change, your UI will remain consistent with the rest of Quip. A typical card UI component is meant to have VALUE_STROKE for the border, VALUE_LIGHT for the background and VALUE for text and interior UI controls (chevrons, etc.). When selected, the background becomes the VALUE and the text/UI controls become WHITE.VALUE.

quip.apps.ui.ColorMap

/**
 * @enum {{
 *   KEY: string,
 *   VALUE: string,
 *   VALUE_LIGHT: string,
 *   VALUE_STROKE: string
 * }}
 */
quip.apps.ui.ColorMap = {
    RED: {
        KEY: "RED",
        LABEL: "Red",
        VALUE: string,
        VALUE_LIGHT: string,
        VALUE_STROKE: string,
    },
    ORANGE: {
        KEY: "ORANGE",
        LABEL: "Orange",
        VALUE: string,
        VALUE_LIGHT: string,
        VALUE_STROKE: string,
    },
    YELLOW: {
        KEY: "YELLOW",
        LABEL: "Yellow",
        VALUE: string,
        VALUE_LIGHT: string,
        VALUE_STROKE: string,
    },
    GREEN: {
        KEY: "GREEN",
        LABEL: "Green",
        VALUE: string,
        VALUE_LIGHT: string,
        VALUE_STROKE: string,
    },
    BLUE: {
        KEY: "BLUE",
        LABEL: "Blue",
        VALUE: string,
        VALUE_LIGHT: string,
        VALUE_STROKE: string,
    },
    VIOLET: {
        KEY: "VIOLET",
        LABEL: "Violet",
        VALUE: string,
        VALUE_LIGHT: string,
        VALUE_STROKE: string,
    },
    WHITE: {
        KEY: "WHITE",
        LABEL: "White",
        VALUE: string,
        VALUE_LIGHT: string,
        VALUE_STROKE: string,
    }
};


Color CSS Classes

These classes specify color, background-color, fill, stroke, and border-color respectively:

// Text color
quip-color-text
quip-bg-color-text
quip-fill-color-text
quip-stroke-color-text
quip-border-color-text

// Secondary color
quip-color-text-secondary
quip-bg-color-text-secondary
quip-fill-color-text-secondary
quip-stroke-color-text-secondary
quip-border-color-text-secondary

// Action color
quip-color-action
quip-bg-color-action
quip-fill-color-action
quip-stroke-color-action
quip-border-color-action

// Selection color
quip-color-selection
quip-bg-color-selection
quip-fill-color-selection
quip-stroke-color-selection
quip-border-color-selection

// Red
quip-color-red
quip-bg-color-red
quip-fill-color-red
quip-stroke-color-red
quip-border-color-red

// Orange
quip-color-orange
quip-bg-color-orange
quip-fill-color-orange
quip-stroke-color-orange
quip-border-color-orange

// Yellow
quip-color-yellow
quip-bg-color-yellow
quip-fill-color-yellow
quip-stroke-color-yellow
quip-border-color-yellow

// Green
quip-color-green
quip-bg-color-green
quip-fill-color-green
quip-stroke-color-green
quip-border-color-green

// Blue
quip-color-blue
quip-bg-color-blue
quip-fill-color-blue
quip-stroke-color-blue
quip-border-color-blue

// Violet
quip-color-violet
quip-bg-color-violet
quip-fill-color-violet
quip-stroke-color-violet
quip-border-color-violet


Text Style CSS Classes

These classes specify font-size and line-height:

quip-text-plain
quip-text-h1
quip-text-h2
quip-text-h3


Auth

Configuration

We offer two types of auth configurations, URL and OAuth2. An app can have multiple auth configurations.


URL

URL configurations are typically useful for authenticating with a company intranet or another service you control. It relies on using cookies, which are set during the login flow, with cross origin AJAX requests to authenticate the API calls. See Cross-Origin Resource Sharing (CORS): Request with Credentials for more details.

Name [required]
String name for the auth configuration. Must only contain alphanumeric characters, dashes, and underscores and be between 1 and 100 characters long. Must be unique for each auth config each app.

Login Url [required]
Url for the login page of your website. When logging in, the user will be redirected to this url, along with a redirect_uri query parameter. Upon a successful login, the user should be redirected to redirect_uri.
If there are errors you want to return to the client, you can redirect the user to redirect_uri with an error query parameter. You can optionally include error_description and error_uri as query parameters to pass along more information.

Example

Name: intranet
Login Url: https://intranet.company.com/login
OAUTH2

We provide a generic OAuth2 implementation that will enable your app to authenicate and communicate with any third-party platforms that supports an OAuth2 API.

Name [required]
String name for the auth configuration. Must only contain alphanumeric characters, dashes, and underscores. Must be unique for each auth each app.

Authorization URL [required]
URL for the Authorization endpoint for the third-part platform.

Token URL [required]
URL for the Authorization endpoint for the third-part platform.

Redirect URL [generated]
URL for the Redirection endpoint. This url is generated using the Name field and will only show up once the auth config is complete. You should copy this url and save it in your client app configuration on the third-party-platform.

Client ID [required]
String client id for your client app on the third-party platform.

Client Secret [required]
String client secret for your client app on the third-party platform.

Scope [optional]
String access token scope for your client app on the third-party platform. It's passed as a query parameter to the authorization url during the auth flow.

Proxy API Domains [optional]
Space delimited list of URL patterns. For platforms that don't support client side API calls, we provide a server side proxy that will relay your api requests and sign them with the correct headers. This list controls what api endpoints your app is allowed to communicate with.
All url patterns must be valid http or https urls.

Refresh Token Strategy [optional, default=NONE]
Platforms tend to differ when it comes to how to refresh an access token with a refresh token. We've implemented the standard strategy as outlined in the OAuth2 spec.

  • NONE: this is the default. This means that you don't need to refresh access tokens, the platform doesn't support refresh tokens, or the format of the refresh token request is not supported.
  • STANDARD: We will send a POST request to the Token Url to refresh an access token with the following parameters:

    {
        "client_id": "...",
        "client_secret": "...",
        "refresh_token": "...",
        "grant_type": "refresh_token",
    }

Example

Name:
"gdrive"

Authorization URL:
"https://accounts.google.com/o/oauth2/v2/auth"

Token URL:
"https://www.googleapis.com/oauth2/v4/token"

Client ID:
"..."

Client Secret:
"..."

Scope:
"https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/drive.metadata.readonly"

Proxy API Domains:
"https://*.googleapis.com"

Auth API

Once auth configurations are completed, you can use the apis below to trigger auth flows or make api requests.

quip.apps.auth

@param {string} authName
String name of the auth configuration.

@return {quip.apps.Auth}
Returns the auth instance if there's one corresponding to the given name and undefined otherwise. Depending on the auth config's type, the returned instance may be a quip.apps.UrlAuth or quip.apps.OAuth2

Example

var auth = quip.apps.auth("gdrive");
quip.apps.UrlAuth

This is the object quip.apps.auth will return if the auth config has the type URL.

quip.apps.UrlAuth.prototype.login

Triggers the login flow, which happens in a pop up window on web and in overlay windows on native apps. The window will close automatically if at the end of login flow.

@param {Object=} params [optional]
Object containing custom query parameters to be sent to the login url during the login flow.

@return {Promise}
Returns a promise that resolves if the login is successful and fails if there's an error.

Example

quip.apps.auth("intranet")
    .login({query: "param"})
    .then(() => console.log("Logged in"))
    .catch(error => console.log("Login failed", error));
Making Cross Origin AJAX Requests

Once the user is successfully logged in, a cookie should be set on the domain of the login url. To make a Cross Origin AJAX request using the cookie credentials, you can use fetch.

Example

fetch("https://intranet.company.com/api", {
    mode: "cors",
    credentials: "include"
}).then(response => response.json());
quip.apps.OAuth2

This is the object quip.apps.auth will return if the auth config has the type OAUTH2.

quip.apps.UrlAuth.prototype.login

Triggers the login flow, which happens in a pop up window on web and in overlay windows on native apps. The window will close automatically if at the end of login flow.

@param {Object=} params [optional]
Object containing custom query parameters to be sent to the authorization url during the login flow.

@return {Promise}
Returns a promise that resolves if the login is successful and fails if there's an error.

Example

quip.apps.auth("gdrive")
    .login({
        "access_type": "offline",
        "prompt": "consent",
        "scope": "https://www.googleapis.com/auth/drive.readonly",
    })
    .then(() => console.log("Logged in"))
    .catch(error => console.log("Login failed", error));
quip.apps.OAuth2.prototype.isLoggedIn

@return {Boolean}
Boolean value that represents whether the user is logged in.

Example

quip.apps.auth("gdrive").isLoggedIn()
// returns True
quip.apps.OAuth2.prototype.getTokenResponse

@return {Object}
Returns the token response returned by the OAuth2 token endpoint during the login flow.

Example

quip.apps.auth("gdrive").getTokenResponse()
// returns {
//     access_token: "...",
//     expires: 3600,
//     token: "Bearer"
// }
quip.apps.OAuth2.prototype.getTokenResponseParam

@param {string} paramName [required]
String name of the field you want to query.

@return {*}
Returns the field in corresponding to the paramName in the token response.

Example

quip.apps.auth("gdrive").getTokenResponseParam("access_token")
// returns "ya29..." 
quip.apps.OAuth2.prototype.refreshToken

@return {Promise<http.Response>}
Returns a promise that resolves to an http.Response object containing the result of the server's attempt to refresh the user's access token. It's required that Refresh Token Strategy is set to a value other than NONE.

Example

quip.apps.auth("gdrive").refreshToken()
    .then(response => console.log(
        response.status,
        response.json()))
quip.apps.OAuth2.prototype.logout

@return {Promise<http.Response>}
Returns a promise that resolves to an http.Response object containing the result of the server's attempt to log out.

Example

quip.apps.auth("gdrive").logout()
    .then(response => console.log(
        response.status,
        response.json()))
quip.apps.OAuth2.prototype.request

Apps that have configured Proxy API Domains can use this method to make api calls to third-party platforms.

@param {RequestParams} params [required]
Returns a promise that resolves to an http.Response object containing the result of the server's attempt to log out.

url: string [required]
String url that matches at least one of the Proxy API Domains.

method: string [optional, default="GET"]
String http method. Can be one of GET, HEAD, POST, PUT, PATCH, DELETE.

query: !Object<string, *> [optional]
Object containing query parameters to be appended to the url

headers: !Object<string, *> [optional]
Object containing custom http headers to be sent with the request.

data: !Object<string, *> [optional]
Object containing data to be sent in the body of the request.

@return {Promise<http.Response>}
Returns a promise that resolves to an http.Response object containing the result of the request.

Example

quip.apps.auth("gdrive").request({
    url: "https://www.googleapis.com/drive/files/root"
}).then(response => response.json());

App Manifest

Required

manifest_version
Integer specifying the version of the manifest file format required by this app package. Should always be 1.

name
String name for the app type.

version_name
Dot-separated string representing the version of this app implementation; may be exposed to users.

version_number
Integer version number representing the version of this app implementation. This value must increase each time a new version of the app is uploaded in the developer portal.

sizing_mode
Choose one of three options to specify how the app will be sized. The term "container" used below refers to the app’s container in the Quip document, which will generally occupy the full document width unless the app is in a side-by-side layout.
fill_container
The app’s width is set to its container’s width and its height is determined by its contents. If "fill_container_max_width" is specified, the app’s width is set to that value when its container’s width is larger.
scale
The app’s width is set to its "initial_width" and its height is determined by its contents. It is expected to always maintain a constant width/height aspect ratio and automatically scales down in smaller containers. It can also optionally be manually resizable.
fit_content
The app’s width and height are both determined by its contents and are independent of its container’s width. If the app overflows past its container’s width, it will become horizontally scrollable.

initial_height
Integer height in pixels for a newly created, empty instance of this app. This value is only used to determine the height of the loading placeholder when the app is created.

Optional

initial_width
Integer width in pixels for a newly created, empty instance of this app. This only applies to app with "scale" or "fit_content" sizing. For apps with "fit_content" sizing, this value is only used to determine the width of the loading placeholder when the app is created.

fill_container_max_width
The maximum width of the app. This only applies to apps with "fill_container" sizing.

js_files
The list of JS files to be injected into the app frame. These are injected in the order they appear in this array. The paths specified here are relative to the root of the package.

css_files
The list of CSS files to be injected into the app frame. These are injected in the order they appear in this array. The paths specified here are relative to the root of the package.

other_resources
A list of relative file path patterns. Files in the package matching these patterns will be uploaded to Quip and will be available as resources to the app. These could include image or font resource files, for example.

csp_sources
Map of CSP directives that should be included in the app. The keys are any of "connect_srcs", "font_srcs", "frame_srcs", "img_srcs", "media_srcs", "script_srcs" or "style_srcs", and the values are lists of domains/keywords that should be allowed for that CSP directive. For example, "script_srcs": ["https://foo.com", "https://bar.com"] will generate the CSP header value "script-src https://foo.com https://bar.com;".
Specifying these will allow JS/CSS/images/fonts/etc to be fetched from remote servers, allow the app to make external AJAX requests, and/or allow the app to embed iframes of its own. However, they may cause the app to not work correctly in an offline native client. The domains must use https or be a known keyword, e.g. "'self'", "'none'".

intercept_url_patterns
List of URL patterns that the editor should intercept when pasting a link and create an instance of this app instead. See for how this data is passed to the app. These URL patterns may include * to represent a wildcard and are otherwise treated as prefixes (only a prefix match is necessary in order to match with a pasted URL). These URL patterns must use a protocol of either http or https. Note that these will only work for apps that are globally available to Quip users.

toolbar_color
String representing the color that the main toolbar menu button should be colored. Should be one of ["red", "orange", "yellow", "green", "blue", "violet"].

allow_tiny_size
Set this to true if the app’s width and/or height will ever need to be less than 50px.

disable_app_level_comments
Setting this to true will prevent commenting on the app as a whole (the comment bubble normally in the margin of the Quip document will not be shown)

Example

{
    "id": "cSVfwAKVsWF",
    "manifest_version": 1,
    "name": "My Cool App",
    "version_name": "1.0",
    "version_number": 1,
    "sizing": "fit_content",
    "initial_height": 300,
    "initial_width": 800,
    "disable_app_level_comments": true,
    "js_files": [
        "dist/app.js"
    ],
    "css_files": [
        "dist/app.css"
    ]
}

Questions?

Ask our team and the Quip developer community over at the Salesforce Stack Exchange.