├── frontend
├── .Rhistory
└── index.js
├── .gitignore
├── .gitattributes
├── .DS_Store
├── block.json
├── .block
├── remote.json
└── obb.remote.json
├── package.json
├── .eslintrc.js
├── LICENSE.md
└── README.md
/frontend/.Rhistory:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /.airtableblocksrc.json
3 | /build
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thekamillionaire/check-out-in-block/HEAD/.DS_Store
--------------------------------------------------------------------------------
/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0",
3 | "frontendEntry": "./frontend/index.js"
4 | }
5 |
--------------------------------------------------------------------------------
/.block/remote.json:
--------------------------------------------------------------------------------
1 | {
2 | "blockId": "blk6uZbFRrrfVfqqV",
3 | "baseId": "app4f1LyZFjCB11Ha"
4 | }
5 |
--------------------------------------------------------------------------------
/.block/obb.remote.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseId": "appRftdoYMV93Bt3M",
3 | "blockId": "blkwtErw1sKPhs4C4"
4 | }
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@airtable/blocks": "0.0.53",
4 | "react": "^16.13.0",
5 | "react-dom": "^16.13.0"
6 | },
7 | "devDependencies": {
8 | "eslint": "^6.8.0",
9 | "eslint-plugin-react": "^7.18.3",
10 | "eslint-plugin-react-hooks": "^2.5.0"
11 | },
12 | "scripts": {
13 | "lint": "eslint frontend"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true,
5 | },
6 | extends: ['eslint:recommended', 'plugin:react/recommended'],
7 | globals: {
8 | Atomics: 'readonly',
9 | SharedArrayBuffer: 'readonly',
10 | },
11 | parserOptions: {
12 | ecmaFeatures: {
13 | jsx: true,
14 | },
15 | ecmaVersion: 2018,
16 | sourceType: 'module',
17 | },
18 | plugins: ['react', 'react-hooks'],
19 | rules: {
20 | 'react/prop-types': 0,
21 | 'react-hooks/rules-of-hooks': 'error',
22 | 'react-hooks/exhaustive-deps': 'warn',
23 | },
24 | settings: {
25 | react: {
26 | version: 'detect',
27 | },
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT No Attribution
2 |
3 | Copyright 2020 Airtable
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6 | associated documentation files (the "Software"), to deal in the Software without restriction,
7 | including without limitation the rights to use, copy, modify, merge, publish, distribute,
8 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
12 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
13 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
14 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
15 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Check Out/In Tracker Block
2 | This block is a customizable wizard which, in its most basic form, allows users to select a **Unit** record and create a new linked record in a **Log** table detailing when that record was "checked out" with relevant fields pre-filled to save time. The wizard just as easily allows users to check that unit back in to close out the log and make that unit available for selection once more. Advanced features include tracking a unit's condition over time (i.e. the unit was in "great" condition when it was checked out, but was in "poor" condition when it was checked back in), and the ability to enable a **Items** table for bases structured where each unit is an instance of a type of item (i.e. 5 "Macbook Pros", 6 "Samsung 32-inch TVs", etc.).
3 |
4 | This block will make day-to-day data entry for inventory managers, IT departments, librarians, equipment rental businesses, and more all that much easier!
5 |
6 | [](http://www.youtube.com/watch?v=AZPdeZNQwO4 "Video preview")
7 |
8 | ## How to remix this block
9 | 1. Create a new base (or you can use an existing base).
10 | 2. Create a new block in your base (see [Create a new block](https://airtable.com/developers/blocks/guides/hello-world-tutorial#create-a-new-block), selecting "Remix from Github" as your template.
11 | 3. From the root of your new block, run `block run`.
12 |
13 | ## Notes
14 | For the optional setting for tracking a unit's condition, make sure that all the condition single select fields have overlapping field option values.
--------------------------------------------------------------------------------
/frontend/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | initializeBlock,
3 | useBase,
4 | useRecords,
5 | useGlobalConfig,
6 | useSettingsButton,
7 | useRecordById,
8 | useRecordActionData,
9 | Box,
10 | Dialog,
11 | Heading,
12 | Icon,
13 | TablePickerSynced,
14 | ViewPickerSynced,
15 | FieldPickerSynced,
16 | SwitchSynced,
17 | expandRecord,
18 | expandRecordList,
19 | expandRecordPickerAsync,
20 | RecordCard,
21 | FormField,
22 | Button,
23 | Select,
24 | Text,
25 | TextButton,
26 | Tooltip,
27 | ViewportConstraint
28 | } from '@airtable/blocks/ui';
29 | import {settingsButton, cursor} from '@airtable/blocks';
30 | import { FieldType, ViewType } from '@airtable/blocks/models';
31 | import React, {useState} from 'react';
32 |
33 | function CheckOutBlock() {
34 | const base = useBase();
35 | const globalConfig = useGlobalConfig();
36 |
37 | const GlobalConfigKeys = {
38 | UNITS_TABLE_ID: "unitsTableId",
39 | UNITS_VIEW_ID: "unitsViewId",
40 | UNITS_CONDITION_FIELD_ID: "unitsConditionFieldId",
41 | UNITS_ORIGIN_DATE_FIELD_ID: "unitsOriginDateFieldId",
42 | UNITS_LINK_ITEMS_FIELD_ID: "unitsLinkItemsFieldId",
43 | UNITS_LINK_LOG_FIELD_ID: "unitsLinkLogFieldId",
44 | OPT_TRACK_ITEMS: "optTrackItems",
45 | OPT_TRACK_CONDITION: "optTrackCondition",
46 | OPT_TRACK_ORIGIN_DATE: "optTrackOriginDate",
47 | LOG_VIEW_ID: "logViewId",
48 | LOG_OUT_CONDITION_FIELD_ID: "logCheckOutConditionFieldId",
49 | LOG_OUT_DATE_FIELD_ID: "logCheckOutDateFieldId",
50 | LOG_IN_CONDITION_FIELD_ID: "logCheckInConditionFieldId",
51 | LOG_IN_DATE_FIELD_ID: "logCheckInDateFieldId"
52 | }
53 |
54 | const unitsTableId = globalConfig.get(GlobalConfigKeys.UNITS_TABLE_ID)
55 | const unitsViewId = globalConfig.get(GlobalConfigKeys.UNITS_VIEW_ID)
56 | const unitsConditionFieldId = globalConfig.get(GlobalConfigKeys.UNITS_CONDITION_FIELD_ID)
57 | const unitsOriginDateFieldId = globalConfig.get(GlobalConfigKeys.UNITS_ORIGIN_DATE_FIELD_ID)
58 | const unitsLinkItemsFieldId = globalConfig.get(GlobalConfigKeys.UNITS_LINK_ITEMS_FIELD_ID)
59 | const unitsLinkLogFieldId = globalConfig.get(GlobalConfigKeys.UNITS_LINK_LOG_FIELD_ID)
60 | const optTrackItems = globalConfig.get(GlobalConfigKeys.OPT_TRACK_ITEMS)
61 | const optTrackCondition = globalConfig.get(GlobalConfigKeys.OPT_TRACK_CONDITION)
62 | const optTrackOriginDate = globalConfig.get(GlobalConfigKeys.OPT_TRACK_ORIGIN_DATE)
63 |
64 | const unitsTable = base.getTableByIdIfExists(unitsTableId)
65 | const unitsLinkLogField = unitsTable ? unitsTable.getFieldByIdIfExists(unitsLinkLogFieldId) : null
66 | const logTableId = unitsLinkLogField ? unitsLinkLogField.options.linkedTableId : null
67 | const logViewId = globalConfig.get(GlobalConfigKeys.LOG_VIEW_ID)
68 | const logCheckOutConditionFieldId = globalConfig.get(GlobalConfigKeys.LOG_OUT_CONDITION_FIELD_ID)
69 | const logCheckOutDateFieldId = globalConfig.get(GlobalConfigKeys.LOG_OUT_DATE_FIELD_ID)
70 | const logCheckInConditionFieldId = globalConfig.get(GlobalConfigKeys.LOG_IN_CONDITION_FIELD_ID)
71 | const logCheckInDateFieldId = globalConfig.get(GlobalConfigKeys.LOG_IN_DATE_FIELD_ID)
72 |
73 | // Check if all settings options have values
74 | const initialSetupDone = (unitsTableId && unitsViewId && unitsLinkLogFieldId && logTableId && logViewId && logCheckOutDateFieldId && logCheckInDateFieldId) && (!optTrackCondition || unitsConditionFieldId && logCheckOutConditionFieldId && logCheckInConditionFieldId) && (!optTrackOriginDate || unitsOriginDateFieldId) && (!optTrackItems || unitsLinkItemsFieldId) ? true : false
75 |
76 | // Enable the settings button
77 | const [isShowingSettings, setIsShowingSettings] = useState(!initialSetupDone);
78 | useSettingsButton(function() {
79 | initialSetupDone && setIsShowingSettings(!isShowingSettings);
80 | });
81 |
82 | // Get tables, view ids, and field ids
83 | const availableUnits = useRecords(unitsTable ? unitsTable.getViewByIdIfExists(unitsViewId) : null)
84 | const unitsConditionsField = unitsTable ? unitsTable.getFieldByIdIfExists(unitsConditionFieldId) : null
85 | const unitsConditions = unitsConditionsField ? unitsConditionsField.options.choices.map(x => x.name) : null
86 | const unitsLinkItemsField = unitsTable ? unitsTable.getFieldByIdIfExists(unitsLinkItemsFieldId) : null
87 |
88 | const itemsTableId = unitsLinkItemsField ? unitsLinkItemsField.options.linkedTableId : null
89 | const itemsTable = base.getTableByIdIfExists(itemsTableId)
90 | const itemsLinkUnitsFieldId = unitsLinkItemsField ? unitsLinkItemsField.options.inverseLinkFieldId : null
91 | const itemsLinkUnitsField = itemsTable ? itemsTable.getFieldByIdIfExists(itemsLinkUnitsFieldId) : null
92 |
93 | const logTable = base.getTableByIdIfExists(logTableId)
94 | const checkedOutRecords = useRecords(logTable ? logTable.getViewByIdIfExists(logViewId) : null)
95 | const logLinkUnitsFieldId = unitsLinkLogField ? unitsLinkLogField.options.inverseLinkFieldId : null
96 |
97 | const logCheckOutConditionField = logTable && logCheckOutConditionFieldId ? logTable.getFieldByIdIfExists(logCheckOutConditionFieldId) : null
98 | const logCheckOutConditions = logCheckOutConditionField ? logCheckOutConditionField.options.choices.map(x => x.name) : null
99 | const logCheckInConditionField = logTable && logCheckInConditionFieldId ? logTable.getFieldByIdIfExists(logCheckInConditionFieldId) : null
100 | const logCheckInConditions = logCheckInConditionField ? logCheckInConditionField.options.choices.map(x => x.name) : null
101 | const logConditions = logCheckInConditions && logCheckOutConditions ? logCheckOutConditions.filter(x => logCheckInConditions.includes(x)) : null
102 | // Return the single select options found in all three conditions fields, prevents errors thrown if trying to copy an incompatable option from another field
103 | const sharedConditions = unitsConditions && logConditions ? unitsConditions.filter(x => logConditions.includes(x)) : null
104 | const bestCondition = sharedConditions ? sharedConditions[0] : null
105 |
106 | const today = new Date()
107 | const truncateText= {overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"}
108 |
109 | // Collect info from record button type press
110 | const recordActionData = useRecordActionData();
111 |
112 | const tableId = recordActionData ? recordActionData.tableId : null
113 | const table = base.getTableByIdIfExists(tableId)
114 | const tableName = table ? table.name : null
115 | const recordId = recordActionData ? recordActionData.recordId : null
116 | const record = useRecordById(table, recordId)
117 |
118 | // The "mode" determines what action buttons are shown, and the variables used in the async functions
119 | const modes = [optTrackItems ? itemsTableId : null, unitsTableId, logTableId]
120 | const mode = modes.indexOf(tableId)
121 |
122 | // Items Actions (variables and functions for use in the action buttons)
123 | const unitsRecordsLinkedToItem = useRecords(record && mode == 0 ? record.selectLinkedRecordsFromCell(itemsLinkUnitsField) : null)
124 | // Returns records which are available (determined by presence in a view), and which have a Condition cell value that can be used in the other two condition fields
125 | const availableLinkedUnits = unitsRecordsLinkedToItem ?
126 | unitsRecordsLinkedToItem.filter(x => availableUnits.map(y => y.id).includes(x.id) && (!optTrackCondition || sharedConditions && sharedConditions.includes(x.getCellValue(unitsConditionFieldId).name))) : null
127 | const anyAvailable = availableLinkedUnits != null && availableLinkedUnits.length ? true : false
128 | const bestAvailable = availableLinkedUnits
129 | ? availableLinkedUnits.sort((a, b) => (
130 | optTrackCondition && unitsConditionFieldId && sharedConditions.indexOf(a.getCellValue(unitsConditionFieldId).name) > sharedConditions.indexOf(b.getCellValue(unitsConditionFieldId).name)
131 | ) ? 1 : -1)[0] : null;
132 |
133 | // Units Actions (variables and functions for use in the action buttons)
134 | const logRecordsLinkedToUnit = useRecords(record && mode == 1 ? record.selectLinkedRecordsFromCell(unitsLinkLogField) : null)
135 | const checkedOutRecordsLinkedToUnit = logRecordsLinkedToUnit ? logRecordsLinkedToUnit.filter(x => checkedOutRecords.map(y => y.id).includes(x.id)) : null
136 | const recordIsAvailable = mode == 1 && availableUnits && availableUnits.map(y => y.id).includes(record.id) ? true : false
137 | const hasHistory = logRecordsLinkedToUnit != null && logRecordsLinkedToUnit.length ? true : false
138 |
139 | const checkCanCreateUnit = unitsTable ? unitsTable.checkPermissionsForCreateRecord() : null
140 | const checkCanUpdateUnit = unitsTable ? unitsTable.checkPermissionsForUpdateRecord() : null
141 |
142 | async function createNewUnit() {
143 | const fieldsAndValues = {
144 | [unitsLinkItemsFieldId]: [{id: record.id}],
145 | ...optTrackCondition && {[unitsConditionFieldId]: {name: bestCondition}},
146 | ...optTrackOriginDate && {[unitsOriginDateFieldId]: today}
147 | }
148 |
149 | const newRecordId = await unitsTable.createRecordAsync(fieldsAndValues)
150 | const query = await unitsTable.selectRecordsAsync()
151 | expandRecord(query.getRecordById(newRecordId))
152 | query.unloadData()
153 | }
154 |
155 | // Log Actions (variables and functions for use in the action buttons)
156 | const conditionsChoices = sharedConditions ? sharedConditions.map(x => {return ({value: x, label: x})}) : []
157 | const recordIsCheckedIn = mode == 2 && checkedOutRecords.map(y => y.id).includes(record.id) ? false : true
158 | const unitsRecordsLinkedToLog = useRecords(record && mode == 2 ? record.selectLinkedRecordsFromCell(logLinkUnitsFieldId) : null)
159 |
160 |
161 | const checkCanCreateLog = logTable ? logTable.checkPermissionsForCreateRecord() : null
162 |
163 | async function viewHistory() {
164 | const recordA = await expandRecordList(logRecordsLinkedToUnit)
165 | }
166 |
167 | async function checkOutUnitAuto() {
168 | const logCheckOutInput = mode == 0 ? bestAvailable : record
169 | const fieldsAndValues = {
170 | [logLinkUnitsFieldId]: [{id: logCheckOutInput.id}],
171 | [logCheckOutDateFieldId]: today,
172 | ...optTrackCondition && {[logCheckOutConditionFieldId]: {name: logCheckOutInput.getCellValue(unitsConditionFieldId).name}},
173 | }
174 |
175 | const newRecordId = await logTable.createRecordAsync(fieldsAndValues)
176 | const query = await logTable.selectRecordsAsync()
177 | expandRecord(query.getRecordById(newRecordId))
178 | query.unloadData()
179 | }
180 |
181 | async function checkOutUnitSelect() {
182 | const logCheckOutInput = await expandRecordPickerAsync(availableLinkedUnits)
183 |
184 | if (logCheckOutInput) {
185 | const fieldsAndValues = {
186 | [logLinkUnitsFieldId]: [{id: logCheckOutInput.id}],
187 | [logCheckOutDateFieldId]: today,
188 | ...optTrackCondition && {[logCheckOutConditionFieldId]: {name: logCheckOutInput.getCellValue(unitsConditionFieldId).name}}
189 | }
190 | const newRecordId = await logTable.createRecordAsync(fieldsAndValues)
191 | const query = await logTable.selectRecordsAsync()
192 | expandRecord(query.getRecordById(newRecordId))
193 | query.unloadData()
194 | }
195 | }
196 |
197 | const checkCanUpdateLog = logTable ? logTable.checkPermissionsForUpdateRecord() : null
198 |
199 | const [isDialogOpen, setIsDialogOpen] = useState(false);
200 | const [newCondition, setNewCondition] = useState(null);
201 |
202 | async function checkInUnit() {
203 | const logCheckInInput = mode == 2 ? record : checkedOutRecordsLinkedToUnit[0]
204 | const fieldsAndValues = {
205 | [logCheckInDateFieldId]: today,
206 | ...optTrackCondition && {[logCheckInConditionFieldId]: {name: newCondition}}
207 | }
208 |
209 | await logTable.updateRecordAsync(logCheckInInput, fieldsAndValues)
210 |
211 | const unitInput = mode == 2 ? unitsRecordsLinkedToLog[0] : record
212 |
213 | if(optTrackCondition && unitInput) {
214 |
215 | await unitsTable.updateRecordAsync(unitInput, {
216 | [unitsConditionFieldId]: {name: newCondition}
217 | })
218 | setNewCondition(null)
219 | }
220 | setIsDialogOpen(false)
221 | }
222 |
223 | const emptyViewportText = !initialSetupDone ? "Please complete all settings" : "Click an action button associated with this block"
224 |
225 | return (
226 |